diff --git a/.ai/instructions.md b/.ai/instructions.md index 5f314a0dc99..681829bae6b 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -51,7 +51,79 @@ This document provides essential context for AI models interacting with this pro * **Naming Conventions:** * **Python:** Follows PEP 8. Use clear, descriptive names following snake_case. - * **C++:** Follows the Google C++ Style Guide. + * **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions): + - Function, method, and variable names: `lower_snake_case` + - Class/struct/enum names: `UpperCamelCase` + - Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE` + - Function-local constants: `lower_snake_case` + - Protected/private fields: `lower_snake_case_with_trailing_underscore_` + - Favor descriptive names over abbreviations + +* **C++ Field Visibility:** + * **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`. + * **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants: + 1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references. + ```cpp + // Helper to find matching string in vector and return its pointer + inline const char *vector_find(const std::vector &vec, const char *value) { + for (const char *item : vec) { + if (strcmp(item, value) == 0) + return item; + } + return nullptr; + } + + class ClimateDevice { + public: + void set_custom_fan_modes(std::initializer_list modes) { + this->custom_fan_modes_ = modes; + this->active_custom_fan_mode_ = nullptr; // Reset when modes change + } + bool set_custom_fan_mode(const char *mode) { + // Find mode in supported list and store that pointer (not the input pointer) + const char *validated_mode = vector_find(this->custom_fan_modes_, mode); + if (validated_mode != nullptr) { + this->active_custom_fan_mode_ = validated_mode; + return true; + } + return false; + } + private: + std::vector custom_fan_modes_; // Pointers to string literals in flash + const char *active_custom_fan_mode_{nullptr}; // Must point to entry in custom_fan_modes_ + }; + ``` + 2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption. + ```cpp + class Buffer { + public: + void resize(size_t new_size) { + auto new_data = std::make_unique(new_size); + if (this->data_) { + std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size)); + } + this->data_ = std::move(new_data); + this->size_ = new_size; // Must stay in sync with data_ + } + private: + std::unique_ptr data_; + size_t size_{0}; // Must match allocated size of data_ + }; + ``` + 3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip. + * **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members. + +* **C++ Preprocessor Directives:** + * **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums. + * **Use `#define` only for:** + - Conditional compilation (`#ifdef`, `#ifndef`) + - Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`) + +* **C++ Additional Conventions:** + * **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`) + * **Indentation:** Use spaces (two per indentation level), not tabs + * **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;` + * **Line length:** Wrap lines at no more than 120 characters * **Component Structure:** * **Standard Files:** @@ -100,8 +172,7 @@ This document provides essential context for AI models interacting with this pro * **C++ Class Pattern:** ```cpp - namespace esphome { - namespace my_component { + namespace esphome::my_component { class MyComponent : public Component { public: @@ -117,8 +188,7 @@ This document provides essential context for AI models interacting with this pro int param_{0}; }; - } // namespace my_component - } // namespace esphome + } // namespace esphome::my_component ``` * **Common Component Examples:** @@ -368,3 +438,45 @@ This document provides essential context for AI models interacting with this pro * **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`. * **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`. * **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags. + +## 8. Public API and Breaking Changes + +* **Public C++ API:** + * **Components**: Only documented features at [esphome.io](https://esphome.io) are public API. Undocumented `public` members are internal. + * **Core/Base Classes** (`esphome/core/`, `Component`, `Sensor`, etc.): All `public` members are public API. + * **Components with Global Accessors** (`global_api_server`, etc.): All `public` members are public API (except config setters). + +* **Public Python API:** + * All documented configuration options at [esphome.io](https://esphome.io) are public API. + * Python code in `esphome/core/` actively used by existing core components is considered stable API. + * Other Python code is internal unless explicitly documented for external component use. + +* **Breaking Changes Policy:** + * Aim for **6-month deprecation window** when possible + * Clean breaks allowed for: signature changes, deep refactorings, resource constraints + * Must document migration path in PR description (generates release notes) + * Blog post required for core/base class changes or significant architectural changes + * Full details: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes + +* **Breaking Change Checklist:** + - [ ] Clear justification (RAM/flash savings, architectural improvement) + - [ ] Explored non-breaking alternatives + - [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++) + - [ ] Documented migration path in PR description with before/after examples + - [ ] Updated all internal usage and esphome-docs + - [ ] Tested backward compatibility during deprecation period + +* **Deprecation Pattern (C++):** + ```cpp + // Remove before 2026.6.0 + ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0") + void old_method() { this->new_method(); } + ``` + +* **Deprecation Pattern (Python):** + ```python + # Remove before 2026.6.0 + if CONF_OLD_KEY in config: + _LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0") + config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate + ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1756d5b7651..16837b3186e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -192,6 +192,11 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} + - name: Restore components graph cache + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .temp/components_graph.json + key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} - name: Determine which tests to run id: determine env: @@ -216,6 +221,12 @@ jobs: echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT + - name: Save components graph cache + if: github.ref == 'refs/heads/dev' + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: .temp/components_graph.json + key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} integration-tests: name: Run integration tests diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index 563d55f42b3..6f4351b2984 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -21,7 +21,7 @@ permissions: jobs: request-codeowner-reviews: name: Run - if: ${{ !github.event.pull_request.draft }} + if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} runs-on: ubuntu-latest steps: - name: Request reviews from component codeowners diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ab938b34369..22739753287 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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 + uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3 with: category: "/language:${{matrix.language}}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5356bffd962..b86d00f2aae 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.3 + rev: v0.14.5 hooks: # Run the linter. - id: ruff diff --git a/CODEOWNERS b/CODEOWNERS index 667a44fc03e..e6970af47cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -155,6 +155,7 @@ esphome/components/esp32_ble_tracker/* @bdraco esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron esphome/components/esp32_hosted/* @swoboda1337 +esphome/components/esp32_hosted/update/* @swoboda1337 esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz @@ -180,7 +181,7 @@ esphome/components/gdk101/* @Szewcson esphome/components/gl_r01_i2c/* @pkejval esphome/components/globals/* @esphome/core esphome/components/gp2y1010au0f/* @zry98 -esphome/components/gp8403/* @jesserockz +esphome/components/gp8403/* @jesserockz @sebydocky esphome/components/gpio/* @esphome/core esphome/components/gpio/one_wire/* @ssieb esphome/components/gps/* @coogle @ximex @@ -205,6 +206,7 @@ esphome/components/hdc2010/* @optimusprimespace @ssieb esphome/components/he60r/* @clydebarrow esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal +esphome/components/hlk_fm22x/* @OnFreund esphome/components/hm3301/* @freekode esphome/components/hmac_md5/* @dwmw2 esphome/components/homeassistant/* @esphome/core @OttoWinter @@ -289,6 +291,7 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp3204/* @rsumner +esphome/components/mcp3221/* @philippderdiedas esphome/components/mcp4461/* @p1ngb4ck esphome/components/mcp4728/* @berfenger esphome/components/mcp47a1/* @jesserockz @@ -393,6 +396,7 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtttl/* @glmnet esphome/components/runtime_stats/* @bdraco +esphome/components/rx8130/* @beormund esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core @@ -479,6 +483,7 @@ esphome/components/template/fan/* @ssieb esphome/components/text/* @mauritskorse esphome/components/thermostat/* @kbx81 esphome/components/time/* @esphome/core +esphome/components/tinyusb/* @kbx81 esphome/components/tlc5947/* @rnauber esphome/components/tlc5971/* @IJIJI esphome/components/tm1621/* @Philippe12 diff --git a/Doxyfile b/Doxyfile index 034fa3fa379..a19120b9da3 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.11.0-dev +PROJECT_NUMBER = 2025.12.0-dev # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index b3008bf8029..7f43333c1bb 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -16,6 +16,11 @@ from . import ( class MemoryAnalyzerCLI(MemoryAnalyzer): """Memory analyzer with CLI-specific report generation.""" + # Symbol size threshold for detailed analysis + SYMBOL_SIZE_THRESHOLD: int = ( + 100 # Show symbols larger than this in detailed analysis + ) + # Column width constants COL_COMPONENT: int = 29 COL_FLASH_TEXT: int = 14 @@ -192,14 +197,21 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" ) - # Top 15 largest core symbols + # All core symbols above threshold lines.append("") - lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:") sorted_core_symbols = sorted( self._esphome_core_symbols, key=lambda x: x[2], reverse=True ) + large_core_symbols = [ + (symbol, demangled, size) + for symbol, demangled, size in sorted_core_symbols + if size > self.SYMBOL_SIZE_THRESHOLD + ] - for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]): + lines.append( + f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):" + ) + for i, (symbol, demangled, size) in enumerate(large_core_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append("=" * self.TABLE_WIDTH) @@ -269,13 +281,15 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append("") - # Show all symbols > 100 bytes for better visibility + # Show all symbols above threshold for better visibility large_symbols = [ - (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100 + (sym, dem, size) + for sym, dem, size in sorted_symbols + if size > self.SYMBOL_SIZE_THRESHOLD ] lines.append( - f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):" + f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" ) for i, (symbol, demangled, size) in enumerate(large_symbols): lines.append(f"{i + 1}. {demangled} ({size:,} B)") diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h index e0975f14bcc..9e034b20cb2 100644 --- a/esphome/components/ags10/ags10.h +++ b/esphome/components/ags10/ags10.h @@ -105,7 +105,7 @@ template class AGS10NewI2cAddressAction : public Action, public: TEMPLATABLE_VALUE(uint8_t, new_address) - void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } + void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } }; enum AGS10SetZeroPointActionMode { @@ -122,7 +122,7 @@ template class AGS10SetZeroPointAction : public Action, p TEMPLATABLE_VALUE(uint16_t, value) TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode) - void play(Ts... x) override { + void play(const Ts &...x) override { switch (this->mode_.value(x...)) { case FACTORY_DEFAULT: this->parent_->set_zero_point_with_factory_defaults(); diff --git a/esphome/components/aic3204/automation.h b/esphome/components/aic3204/automation.h index 416a88fa127..851ff930f8a 100644 --- a/esphome/components/aic3204/automation.h +++ b/esphome/components/aic3204/automation.h @@ -13,7 +13,7 @@ template class SetAutoMuteAction : public Action { TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) - void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } + void play(const Ts &...x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } protected: AIC3204 *aic3204_; diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 9f1485ee905..c29e02c8efd 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -1,6 +1,8 @@ -#include - #include "alarm_control_panel.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" + +#include #include "esphome/core/application.h" #include "esphome/core/helpers.h" @@ -34,6 +36,9 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); this->current_state_ = state; this->state_callback_.call(); +#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_alarm_control_panel_update(this); +#endif if (state == ACP_STATE_TRIGGERED) { this->triggered_callback_.call(); } else if (state == ACP_STATE_ARMING) { diff --git a/esphome/components/alarm_control_panel/automation.h b/esphome/components/alarm_control_panel/automation.h index 2177fb710f0..db2ef781582 100644 --- a/esphome/components/alarm_control_panel/automation.h +++ b/esphome/components/alarm_control_panel/automation.h @@ -89,7 +89,7 @@ template class ArmAwayAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->alarm_control_panel_->make_call(); auto code = this->code_.optional_value(x...); if (code.has_value()) { @@ -109,7 +109,7 @@ template class ArmHomeAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->alarm_control_panel_->make_call(); auto code = this->code_.optional_value(x...); if (code.has_value()) { @@ -129,7 +129,7 @@ template class ArmNightAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->alarm_control_panel_->make_call(); auto code = this->code_.optional_value(x...); if (code.has_value()) { @@ -149,7 +149,7 @@ template class DisarmAction : public Action { TEMPLATABLE_VALUE(std::string, code) - void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } + void play(const Ts &...x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } protected: AlarmControlPanel *alarm_control_panel_; @@ -159,7 +159,7 @@ template class PendingAction : public Action { public: explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} - void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); } + void play(const Ts &...x) override { this->alarm_control_panel_->make_call().pending().perform(); } protected: AlarmControlPanel *alarm_control_panel_; @@ -169,7 +169,7 @@ template class TriggeredAction : public Action { public: explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} - void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); } + void play(const Ts &...x) override { this->alarm_control_panel_->make_call().triggered().perform(); } protected: AlarmControlPanel *alarm_control_panel_; @@ -178,7 +178,7 @@ template class TriggeredAction : public Action { template class AlarmControlPanelCondition : public Condition { public: AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { return this->parent_->is_state_armed(this->parent_->get_state()) || this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; } diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h index c44e0060aff..b33254df30f 100644 --- a/esphome/components/animation/animation.h +++ b/esphome/components/animation/animation.h @@ -39,7 +39,7 @@ class Animation : public image::Image { template class AnimationNextFrameAction : public Action { public: AnimationNextFrameAction(Animation *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->next_frame(); } + void play(const Ts &...x) override { this->parent_->next_frame(); } protected: Animation *parent_; @@ -48,7 +48,7 @@ template class AnimationNextFrameAction : public Action { template class AnimationPrevFrameAction : public Action { public: AnimationPrevFrameAction(Animation *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->prev_frame(); } + void play(const Ts &...x) override { this->parent_->prev_frame(); } protected: Animation *parent_; @@ -58,7 +58,7 @@ template class AnimationSetFrameAction : public Action { public: AnimationSetFrameAction(Animation *parent) : parent_(parent) {} TEMPLATABLE_VALUE(uint16_t, frame) - void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_frame(this->frame_.value(x...)); } protected: Animation *parent_; diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 449572c0e51..a9286c531fa 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -227,6 +227,7 @@ CONFIG_SCHEMA = cv.All( esp32=8, # More RAM, can buffer more rp2040=5, # Limited RAM bk72xx=8, # Moderate RAM + nrf52=8, # Moderate RAM rtl87xx=8, # Moderate RAM host=16, # Abundant resources ln882x=8, # Moderate RAM @@ -244,6 +245,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Track controller registration for StaticVector sizing + CORE.register_controller() + cg.add(var.set_port(config[CONF_PORT])) if config[CONF_PASSWORD]: cg.add_define("USE_API_PASSWORD") diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 7a50fa6b179..e115e4630d8 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -2147,7 +2147,7 @@ message ListEntitiesEventResponse { EntityCategory entity_category = 7; string device_class = 8; - repeated string event_types = 9; + repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; } message EventResponse { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5ab8a6eb050..4acd2fc15c5 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -476,8 +476,9 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c auto *light = static_cast(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); + auto supported_modes = traits.get_supported_color_modes(); // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values - msg.supported_color_modes = &traits.get_supported_color_modes(); + msg.supported_color_modes = &supported_modes; if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { msg.min_mireds = traits.get_min_mireds(); @@ -1294,11 +1295,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe #endif #ifdef USE_EVENT -void APIConnection::send_event(event::Event *event, const std::string &event_type) { - this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, - EventResponse::ESTIMATED_SIZE); +void APIConnection::send_event(event::Event *event, const char *event_type) { + this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, + EventResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, +uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single) { EventResponse resp; resp.set_event_type(StringRef(event_type)); @@ -1310,8 +1311,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c auto *event = static_cast(entity); ListEntitiesEventResponse msg; msg.set_device_class(event->get_device_class_ref()); - for (const auto &event_type : event->get_event_types()) - msg.event_types.push_back(event_type); + msg.event_types = &event->get_event_types(); return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1468,6 +1468,8 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { static constexpr auto MANUFACTURER = StringRef::from_lit("Beken"); #elif defined(USE_LN882X) static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning"); +#elif defined(USE_NRF52) + static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor"); #elif defined(USE_RTL87XX) static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek"); #elif defined(USE_HOST) @@ -1649,9 +1651,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c // O(n) but optimized for RAM and not performance. for (auto &item : items) { if (item.entity == entity && item.message_type == message_type) { - // Clean up old creator before replacing - item.creator.cleanup(message_type); - // Move assign the new creator + // Replace with new creator item.creator = std::move(creator); return; } @@ -1821,7 +1821,7 @@ void APIConnection::process_batch_() { // Handle remaining items more efficiently if (items_processed < this->deferred_batch_.size()) { - // Remove processed items from the beginning with proper cleanup + // Remove processed items from the beginning this->deferred_batch_.remove_front(items_processed); // Reschedule for remaining items this->schedule_batch_(); @@ -1834,10 +1834,10 @@ void APIConnection::process_batch_() { uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const { #ifdef USE_EVENT - // Special case: EventResponse uses string pointer + // Special case: EventResponse uses const char * pointer if (message_type == EventResponse::MESSAGE_TYPE) { auto *e = static_cast(entity); - return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single); + return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single); } #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 284fa11a954..6cfd1089272 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection { #endif #ifdef USE_EVENT - void send_event(event::Event *event, const std::string &event_type); + void send_event(event::Event *event, const char *event_type); #endif #ifdef USE_UPDATE @@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection { bool is_single); #endif #ifdef USE_EVENT - static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn, + static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint32_t remaining_size, bool is_single); static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif @@ -508,10 +508,8 @@ class APIConnection final : public APIServerConnection { // Constructor for function pointer MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } - // Constructor for string state capture - explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); } - - // No destructor - cleanup must be called explicitly with message_type + // Constructor for const char * (Event types - no allocation needed) + explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; } // Delete copy operations - MessageCreator should only be moved MessageCreator(const MessageCreator &other) = delete; @@ -523,8 +521,6 @@ class APIConnection final : public APIServerConnection { // Move assignment MessageCreator &operator=(MessageCreator &&other) noexcept { if (this != &other) { - // IMPORTANT: Caller must ensure cleanup() was called if this contains a string! - // In our usage, this happens in add_item() deduplication and vector::erase() data_ = other.data_; other.data_.function_ptr = nullptr; } @@ -535,20 +531,10 @@ class APIConnection final : public APIServerConnection { uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint8_t message_type) const; - // Manual cleanup method - must be called before destruction for string types - void cleanup(uint8_t message_type) { -#ifdef USE_EVENT - if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) { - delete data_.string_ptr; - data_.string_ptr = nullptr; - } -#endif - } - private: union Data { MessageCreatorPtr function_ptr; - std::string *string_ptr; + const char *const_char_ptr; } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before }; @@ -568,42 +554,24 @@ class APIConnection final : public APIServerConnection { std::vector items; uint32_t batch_start_time{0}; - private: - // Helper to cleanup items from the beginning - void cleanup_items_(size_t count) { - for (size_t i = 0; i < count; i++) { - items[i].creator.cleanup(items[i].message_type); - } - } - - public: DeferredBatch() { // Pre-allocate capacity for typical batch sizes to avoid reallocation items.reserve(8); } - ~DeferredBatch() { - // Ensure cleanup of any remaining items - clear(); - } - // Add item to the batch void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); // Add item to the front of the batch (for high priority messages like ping) void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); - // Clear all items with proper cleanup + // Clear all items void clear() { - cleanup_items_(items.size()); items.clear(); batch_start_time = 0; } - // Remove processed items from the front with proper cleanup - void remove_front(size_t count) { - cleanup_items_(count); - items.erase(items.begin(), items.begin() + count); - } + // Remove processed items from the front + void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } bool empty() const { return items.empty(); } size_t size() const { return items.size(); } @@ -682,21 +650,30 @@ class APIConnection final : public APIServerConnection { } #endif + // Helper to check if a message type should bypass batching + // Returns true if: + // 1. It's an UpdateStateResponse (always send immediately to handle cases where + // the main loop is blocked, e.g., during OTA updates) + // 2. It's an EventResponse (events are edge-triggered - every occurrence matters) + // 3. OR: User has opted into immediate sending (should_try_send_immediately = true + // AND batch_delay = 0) + inline bool should_send_immediately_(uint8_t message_type) const { + return ( +#ifdef USE_UPDATE + message_type == UpdateStateResponse::MESSAGE_TYPE || +#endif +#ifdef USE_EVENT + message_type == EventResponse::MESSAGE_TYPE || +#endif + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)); + } + // Helper method to send a message either immediately or via batching + // Tries immediate send if should_send_immediately_() returns true and buffer has space + // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, uint8_t estimated_size) { - // Try to send immediately if: - // 1. It's an UpdateStateResponse (always send immediately to handle cases where - // the main loop is blocked, e.g., during OTA updates) - // 2. OR: We should try to send immediately (should_try_send_immediately = true) - // AND Batch delay is 0 (user has opted in to immediate sending) - // 3. AND: Buffer has space available - if (( -#ifdef USE_UPDATE - message_type == UpdateStateResponse::MESSAGE_TYPE || -#endif - (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && - this->helper_->can_write_without_blocking()) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { @@ -714,6 +691,27 @@ class APIConnection final : public APIServerConnection { return this->schedule_message_(entity, creator, message_type, estimated_size); } + // Overload for MessageCreator (used by events which need to capture event_type) + bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { + // Try to send immediately if message type should bypass batching and buffer has space + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + // Now actually encode and send + if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) && + this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + // Log the message in verbose mode + this->log_proto_message_(entity, creator, message_type); +#endif + return true; + } + + // If immediate send failed, fall through to batching + } + + // Fall back to scheduled batching + return this->schedule_message_(entity, std::move(creator), message_type, estimated_size); + } + // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) { this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index dfa1a1320fe..0a073fb662f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2877,8 +2877,8 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->disabled_by_default); buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_string(8, this->device_class_ref_); - for (auto &it : this->event_types) { - buffer.encode_string(9, it, true); + for (const char *it : *this->event_types) { + buffer.encode_string(9, it, strlen(it), true); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -2894,9 +2894,9 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const { size.add_bool(1, this->disabled_by_default); size.add_uint32(1, static_cast(this->entity_category)); size.add_length(1, this->device_class_ref_.size()); - if (!this->event_types.empty()) { - for (const auto &it : this->event_types) { - size.add_length_force(1, it.size()); + if (!this->event_types->empty()) { + for (const char *it : *this->event_types) { + size.add_length_force(1, strlen(it)); } } #ifdef USE_DEVICES diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 716f1a6e9b5..358049026ec 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2788,7 +2788,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage { #endif StringRef device_class_ref_{}; void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } - std::vector event_types{}; + const FixedVector *event_types{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index d94ceaaa9c4..d9662483bf5 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2053,7 +2053,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const { dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "entity_category", static_cast(this->entity_category)); dump_field(out, "device_class", this->device_class_ref_); - for (const auto &it : this->event_types) { + for (const auto &it : *this->event_types) { dump_field(out, "event_types", it, 4); } #ifdef USE_DEVICES diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index e618610a75d..18601d74ff4 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -5,6 +5,7 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -34,7 +35,7 @@ APIServer::APIServer() { } void APIServer::setup() { - this->setup_controller(); + ControllerRegistry::register_controller(this); #ifdef USE_API_NOISE uint32_t hash = 88491486UL; @@ -224,7 +225,7 @@ void APIServer::dump_config() { " Address: %s:%u\n" " Listen backlog: %u\n" " Max connections: %u", - network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_); + network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); #ifdef USE_API_NOISE ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); if (!this->noise_ctx_->has_psk()) { @@ -269,7 +270,7 @@ bool APIServer::check_password(const uint8_t *password_data, size_t password_len void APIServer::handle_disconnect(APIConnection *conn) {} -// Macro for entities without extra parameters +// Macro for controller update dispatch #define API_DISPATCH_UPDATE(entity_type, entity_name) \ void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ if (obj->is_internal()) \ @@ -278,15 +279,6 @@ void APIServer::handle_disconnect(APIConnection *conn) {} c->send_##entity_name##_state(obj); \ } -// Macro for entities with extra parameters (but parameters not used in send) -#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \ - void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \ - if (obj->is_internal()) \ - return; \ - for (auto &c : this->clients_) \ - c->send_##entity_name##_state(obj); \ - } - #ifdef USE_BINARY_SENSOR API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) #endif @@ -304,15 +296,15 @@ API_DISPATCH_UPDATE(light::LightState, light) #endif #ifdef USE_SENSOR -API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state) +API_DISPATCH_UPDATE(sensor::Sensor, sensor) #endif #ifdef USE_SWITCH -API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state) +API_DISPATCH_UPDATE(switch_::Switch, switch) #endif #ifdef USE_TEXT_SENSOR -API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state) +API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor) #endif #ifdef USE_CLIMATE @@ -320,7 +312,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate) #endif #ifdef USE_NUMBER -API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state) +API_DISPATCH_UPDATE(number::Number, number) #endif #ifdef USE_DATETIME_DATE @@ -336,11 +328,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime) #endif #ifdef USE_TEXT -API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state) +API_DISPATCH_UPDATE(text::Text, text) #endif #ifdef USE_SELECT -API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index) +API_DISPATCH_UPDATE(select::Select, select) #endif #ifdef USE_LOCK @@ -356,12 +348,13 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) #endif #ifdef USE_EVENT -// Event is a special case - it's the only entity that passes extra parameters to the send method -void APIServer::on_event(event::Event *obj, const std::string &event_type) { +// Event is a special case - unlike other entities with simple state fields, +// events store their state in a member accessed via obj->get_last_event_type() +void APIServer::on_event(event::Event *obj) { if (obj->is_internal()) return; for (auto &c : this->clients_) - c->send_event(obj, event_type); + c->send_event(obj, obj->get_last_event_type()); } #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index d29181250e7..2d58063d6cf 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -72,19 +72,19 @@ class APIServer : public Component, public Controller { void on_light_update(light::LightState *obj) override; #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj, float state) override; + void on_sensor_update(sensor::Sensor *obj) override; #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj, bool state) override; + void on_switch_update(switch_::Switch *obj) override; #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; + void on_text_sensor_update(text_sensor::TextSensor *obj) override; #endif #ifdef USE_CLIMATE void on_climate_update(climate::Climate *obj) override; #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj, float state) override; + void on_number_update(number::Number *obj) override; #endif #ifdef USE_DATETIME_DATE void on_date_update(datetime::DateEntity *obj) override; @@ -96,10 +96,10 @@ class APIServer : public Component, public Controller { void on_datetime_update(datetime::DateTimeEntity *obj) override; #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj, const std::string &state) override; + void on_text_update(text::Text *obj) override; #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state, size_t index) override; + void on_select_update(select::Select *obj) override; #endif #ifdef USE_LOCK void on_lock_update(lock::Lock *obj) override; @@ -141,7 +141,7 @@ class APIServer : public Component, public Controller { void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; #endif #ifdef USE_EVENT - void on_event(event::Event *obj, const std::string &event_type) override; + void on_event(event::Event *obj) override; #endif #ifdef USE_UPDATE void on_update(update::UpdateEntity *obj) override; @@ -237,7 +237,7 @@ extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-cons template class APIConnectedCondition : public Condition { public: - bool check(Ts... x) override { return global_api_server->is_connected(); } + bool check(const Ts &...x) override { return global_api_server->is_connected(); } }; } // namespace esphome::api diff --git a/esphome/components/api/custom_api_device.h b/esphome/components/api/custom_api_device.h index d34ccfa0cef..43ea644f0c3 100644 --- a/esphome/components/api/custom_api_device.h +++ b/esphome/components/api/custom_api_device.h @@ -9,11 +9,11 @@ namespace esphome::api { #ifdef USE_API_SERVICES -template class CustomAPIDeviceService : public UserServiceBase { +template class CustomAPIDeviceService : public UserServiceDynamic { public: CustomAPIDeviceService(const std::string &name, const std::array &arg_names, T *obj, void (T::*callback)(Ts...)) - : UserServiceBase(name, arg_names), obj_(obj), callback_(callback) {} + : UserServiceDynamic(name, arg_names), obj_(obj), callback_(callback) {} protected: void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 4343fcd0bba..d00e9e62570 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -133,7 +133,7 @@ template class HomeAssistantServiceCallAction : public Action *get_error_trigger() const { return this->error_trigger_; } #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES - void play(Ts... x) override { + void play(const Ts &...x) override { HomeassistantActionRequest resp; std::string service_value = this->service_.value(x...); resp.set_service(StringRef(service_value)); diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index 9ca5e1093e0..2f753dfc6c8 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -23,11 +23,13 @@ template T get_execute_arg_value(const ExecuteServiceArgument &arg); template enums::ServiceArgType to_service_arg_type(); +// Base class for YAML-defined services (most common case) +// Stores only pointers to string literals in flash - no heap allocation template class UserServiceBase : public UserServiceDescriptor { public: - UserServiceBase(std::string name, const std::array &arg_names) - : name_(std::move(name)), arg_names_(arg_names) { - this->key_ = fnv1_hash(this->name_); + UserServiceBase(const char *name, const std::array &arg_names) + : name_(name), arg_names_(arg_names) { + this->key_ = fnv1_hash(name); } ListEntitiesServicesResponse encode_list_service_response() override { @@ -47,26 +49,74 @@ template class UserServiceBase : public UserServiceDescriptor { bool execute_service(const ExecuteServiceRequest &req) override { if (req.key != this->key_) return false; - if (req.args.size() != this->arg_names_.size()) + if (req.args.size() != sizeof...(Ts)) return false; - this->execute_(req.args, typename gens::type()); + this->execute_(req.args, std::make_index_sequence{}); return true; } protected: virtual void execute(Ts... x) = 0; - template void execute_(const ArgsContainer &args, seq type) { + template + void execute_(const ArgsContainer &args, std::index_sequence type) { this->execute((get_execute_arg_value(args[S]))...); } - std::string name_; + // Pointers to string literals in flash - no heap allocation + const char *name_; + std::array arg_names_; uint32_t key_{0}; +}; + +// Separate class for custom_api_device services (rare case) +// Stores copies of runtime-generated names +template class UserServiceDynamic : public UserServiceDescriptor { + public: + UserServiceDynamic(std::string name, const std::array &arg_names) + : name_(std::move(name)), arg_names_(arg_names) { + this->key_ = fnv1_hash(this->name_.c_str()); + } + + ListEntitiesServicesResponse encode_list_service_response() override { + ListEntitiesServicesResponse msg; + msg.set_name(StringRef(this->name_)); + msg.key = this->key_; + std::array arg_types = {to_service_arg_type()...}; + msg.args.init(sizeof...(Ts)); + for (size_t i = 0; i < sizeof...(Ts); i++) { + auto &arg = msg.args.emplace_back(); + arg.type = arg_types[i]; + arg.set_name(StringRef(this->arg_names_[i])); + } + return msg; + } + + bool execute_service(const ExecuteServiceRequest &req) override { + if (req.key != this->key_) + return false; + if (req.args.size() != sizeof...(Ts)) + return false; + this->execute_(req.args, std::make_index_sequence{}); + return true; + } + + protected: + virtual void execute(Ts... x) = 0; + template + void execute_(const ArgsContainer &args, std::index_sequence type) { + this->execute((get_execute_arg_value(args[S]))...); + } + + // Heap-allocated strings for runtime-generated names + std::string name_; std::array arg_names_; + uint32_t key_{0}; }; template class UserServiceTrigger : public UserServiceBase, public Trigger { public: - UserServiceTrigger(const std::string &name, const std::array &arg_names) + // Constructor for static names (YAML-defined services - used by code generator) + UserServiceTrigger(const char *name, const std::array &arg_names) : UserServiceBase(name, arg_names) {} protected: diff --git a/esphome/components/at581x/automation.h b/esphome/components/at581x/automation.h index 4863a875651..b1611a6758f 100644 --- a/esphome/components/at581x/automation.h +++ b/esphome/components/at581x/automation.h @@ -10,7 +10,7 @@ namespace at581x { template class AT581XResetAction : public Action, public Parented { public: - void play(Ts... x) { this->parent_->reset_hardware_frontend(); } + void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); } }; template class AT581XSettingsAction : public Action, public Parented { @@ -25,7 +25,7 @@ template class AT581XSettingsAction : public Action, publ TEMPLATABLE_VALUE(int, trigger_keep) TEMPLATABLE_VALUE(int, stage_gain) - void play(Ts... x) { + void play(const Ts &...x) { if (this->frequency_.has_value()) { int v = this->frequency_.value(x...); this->parent_->set_frequency(v); diff --git a/esphome/components/audio_adc/automation.h b/esphome/components/audio_adc/automation.h index 1b0bc2a6ad8..0c424684793 100644 --- a/esphome/components/audio_adc/automation.h +++ b/esphome/components/audio_adc/automation.h @@ -13,7 +13,7 @@ template class SetMicGainAction : public Action { TEMPLATABLE_VALUE(float, mic_gain) - void play(Ts... x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } + void play(const Ts &...x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } protected: AudioAdc *audio_adc_; diff --git a/esphome/components/audio_dac/automation.h b/esphome/components/audio_dac/automation.h index b6cf2acaf4c..3eb3441f3d3 100644 --- a/esphome/components/audio_dac/automation.h +++ b/esphome/components/audio_dac/automation.h @@ -11,7 +11,7 @@ template class MuteOffAction : public Action { public: explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} - void play(Ts... x) override { this->audio_dac_->set_mute_off(); } + void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); } protected: AudioDac *audio_dac_; @@ -21,7 +21,7 @@ template class MuteOnAction : public Action { public: explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} - void play(Ts... x) override { this->audio_dac_->set_mute_on(); } + void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); } protected: AudioDac *audio_dac_; @@ -33,7 +33,7 @@ template class SetVolumeAction : public Action { TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } protected: AudioDac *audio_dac_; diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index 0693be10925..10f403dd1ac 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -100,7 +100,6 @@ enum BedjetCommand : uint8_t { static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; -static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; } // namespace bedjet } // namespace esphome diff --git a/esphome/components/bedjet/climate/bedjet_climate.cpp b/esphome/components/bedjet/climate/bedjet_climate.cpp index 877fd6f771e..716d4d42410 100644 --- a/esphome/components/bedjet/climate/bedjet_climate.cpp +++ b/esphome/components/bedjet/climate/bedjet_climate.cpp @@ -8,15 +8,15 @@ namespace bedjet { using namespace esphome::climate; -static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { +static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { if (fan_step < BEDJET_FAN_SPEED_COUNT) - return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; + return BEDJET_FAN_STEP_NAMES[fan_step]; return nullptr; } -static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { +static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) { for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) { - if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { + if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) { return i; } } @@ -163,27 +163,27 @@ void BedJetClimate::control(const ClimateCall &call) { ESP_LOGW(TAG, "Unsupported preset: %d", preset); return; } - } else if (call.get_custom_preset().has_value()) { - std::string preset = *call.get_custom_preset(); + } else if (call.has_custom_preset()) { + const char *preset = call.get_custom_preset(); bool result; - if (preset == "M1") { + if (strcmp(preset, "M1") == 0) { result = this->parent_->button_memory1(); - } else if (preset == "M2") { + } else if (strcmp(preset, "M2") == 0) { result = this->parent_->button_memory2(); - } else if (preset == "M3") { + } else if (strcmp(preset, "M3") == 0) { result = this->parent_->button_memory3(); - } else if (preset == "LTD HT") { + } else if (strcmp(preset, "LTD HT") == 0) { result = this->parent_->button_heat(); - } else if (preset == "EXT HT") { + } else if (strcmp(preset, "EXT HT") == 0) { result = this->parent_->button_ext_heat(); } else { - ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); + ESP_LOGW(TAG, "Unsupported preset: %s", preset); return; } if (result) { - this->set_custom_preset_(preset.c_str()); + this->set_custom_preset_(preset); } } @@ -207,15 +207,14 @@ void BedJetClimate::control(const ClimateCall &call) { if (result) { this->set_fan_mode_(fan_mode); } - } else if (call.get_custom_fan_mode().has_value()) { - auto fan_mode = *call.get_custom_fan_mode(); + } else if (call.has_custom_fan_mode()) { + const char *fan_mode = call.get_custom_fan_mode(); auto fan_index = bedjet_fan_speed_to_step(fan_mode); if (fan_index <= 19) { - ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), - fan_index); + ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index); bool result = this->parent_->set_fan_index(fan_index); if (result) { - this->set_custom_fan_mode_(fan_mode.c_str()); + this->set_custom_fan_mode_(fan_mode); } } } @@ -241,7 +240,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); if (fan_mode_name != nullptr) { - this->set_custom_fan_mode_(fan_mode_name->c_str()); + this->set_custom_fan_mode_(fan_mode_name); } // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index 0bc7b9acb37..f6971a2fc4a 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -141,7 +141,7 @@ class StateChangeTrigger : public Trigger, optional > { template class BinarySensorCondition : public Condition { public: BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} - bool check(Ts... x) override { return this->parent_->state == this->state_; } + bool check(const Ts &...x) override { return this->parent_->state == this->state_; } protected: BinarySensor *parent_; @@ -153,7 +153,7 @@ template class BinarySensorPublishAction : public Action explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { auto val = this->state_.value(x...); this->sensor_->publish_state(val); } @@ -166,7 +166,7 @@ template class BinarySensorInvalidateAction : public Actionsensor_->invalidate_state(); } + void play(const Ts &...x) override { this->sensor_->invalidate_state(); } protected: BinarySensor *sensor_; diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 33b3de6d72b..220ed685db0 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -1,4 +1,6 @@ #include "binary_sensor.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -37,6 +39,9 @@ void BinarySensor::send_state_internal(bool new_state) { // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed if (this->set_state_(new_state)) { ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); +#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_binary_sensor_update(this); +#endif } } diff --git a/esphome/components/bl0906/bl0906.h b/esphome/components/bl0906/bl0906.h index 5a9ad0f0280..493b645c89d 100644 --- a/esphome/components/bl0906/bl0906.h +++ b/esphome/components/bl0906/bl0906.h @@ -89,7 +89,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice { template class ResetEnergyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } + void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } }; } // namespace bl0906 diff --git a/esphome/components/bl0940/number/calibration_number.cpp b/esphome/components/bl0940/number/calibration_number.cpp index cdb26cd2982..e83c3add1fb 100644 --- a/esphome/components/bl0940/number/calibration_number.cpp +++ b/esphome/components/bl0940/number/calibration_number.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number"; void CalibrationNumber::setup() { float value = 0.0f; if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->pref_.load(&value)) { value = 0.0f; } diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 768a345213d..37db181584b 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VALUE, ) +from esphome.core import ID AUTO_LOAD = ["esp32_ble_client"] CODEOWNERS = ["@buxtronix", "@clydebarrow"] @@ -198,7 +199,12 @@ async def ble_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_value_template(templ)) else: - cg.add(var.set_value_simple(value)) + # Generate static array in flash to avoid RAM copy + if isinstance(value, bytes): + value = list(value) + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value)) + cg.add(var.set_value_simple(arr, len(value))) if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): cg.add( diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 55f1cb2f460..bbc2dd05e0e 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -96,11 +96,8 @@ template class BLEClientWriteAction : public Action, publ BLEClientWriteAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; - this->construct_simple_value_(); } - ~BLEClientWriteAction() { this->destroy_simple_value_(); } - void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } @@ -110,27 +107,34 @@ template class BLEClientWriteAction : public Action, publ void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_value_template(std::vector (*func)(Ts...)) { - this->destroy_simple_value_(); - this->value_.template_func = func; - this->has_simple_value_ = false; + this->value_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_value_simple(const std::vector &value) { - if (!this->has_simple_value_) { - this->construct_simple_value_(); - } - this->value_.simple = value; - this->has_simple_value_ = true; + // Store pointer to static data in flash (no RAM copy) + void set_value_simple(const uint8_t *data, size_t len) { + this->value_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } - void play(Ts... x) override {} + void play(const Ts &...x) override {} - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; this->var_ = std::make_tuple(x...); - auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...); + + bool result; + if (this->len_ >= 0) { + // Static mode: write directly from flash pointer + result = this->write(this->value_.data, this->len_); + } else { + // Template mode: call function and write the vector + std::vector value = this->value_.func(x...); + result = this->write(value); + } + // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. - if (!write(value)) + if (!result) this->play_next_(x...); } @@ -143,15 +147,15 @@ template class BLEClientWriteAction : public Action, publ * errors. */ // initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event. - bool write(const std::vector &value) { + bool write(const uint8_t *data, size_t len) { if (this->node_state != espbt::ClientState::ESTABLISHED) { esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected"); return false; } - esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str()); - esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), - this->char_handle_, value.size(), const_cast(value.data()), - this->write_type_, ESP_GATT_AUTH_REQ_NONE); + esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str()); + esp_err_t err = + esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len, + const_cast(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE); if (err != ESP_OK) { esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); return false; @@ -159,6 +163,8 @@ template class BLEClientWriteAction : public Action, publ return true; } + bool write(const std::vector &value) { return this->write(value.data(), value.size()); } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { switch (event) { @@ -201,21 +207,11 @@ template class BLEClientWriteAction : public Action, publ } private: - void construct_simple_value_() { new (&this->value_.simple) std::vector(); } - - void destroy_simple_value_() { - if (this->has_simple_value_) { - this->value_.simple.~vector(); - } - } - BLEClient *ble_client_; - bool has_simple_value_ = true; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length union Value { - std::vector simple; - std::vector (*template_func)(Ts...); - Value() {} // trivial constructor - ~Value() {} // trivial destructor - we manage lifetime via discriminator + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash } value_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; @@ -229,7 +225,7 @@ template class BLEClientPasskeyReplyAction : public Actionvalue_.simple; @@ -266,7 +262,7 @@ template class BLEClientNumericComparisonReplyAction : public Ac public: BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; } - void play(Ts... x) override { + void play(const Ts &...x) override { esp_bd_addr_t remote_bda; memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); if (has_simple_value_) { @@ -299,7 +295,7 @@ template class BLEClientRemoveBondAction : public Action public: BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; } - void play(Ts... x) override { + void play(const Ts &...x) override { esp_bd_addr_t remote_bda; memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); esp_ble_remove_bond_device(remote_bda); @@ -334,9 +330,9 @@ template class BLEClientConnectAction : public Action, pu } // not used since we override play_complex_ - void play(Ts... x) override {} + void play(const Ts &...x) override {} - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { // it makes no sense to have multiple instances of this running at the same time. // this would occur only if the same automation was re-triggered while still // running. So just cancel the second chain if this is detected. @@ -379,9 +375,9 @@ template class BLEClientDisconnectAction : public Action, } // not used since we override play_complex_ - void play(Ts... x) override {} + void play(const Ts &...x) override {} - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; if (this->node_state == espbt::ClientState::IDLE) { this->play_next_(x...); diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index 6d293528c68..61685c05665 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -77,6 +77,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga } } else { this->node_state = espbt::ClientState::ESTABLISHED; + // For non-notify characteristics, trigger an immediate read after service discovery + // to avoid peripherals disconnecting due to inactivity + this->update(); } break; } diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp index e7da297fa05..b7a6d154dbb 100644 --- a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -79,6 +79,9 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ } } else { this->node_state = espbt::ClientState::ESTABLISHED; + // For non-notify characteristics, trigger an immediate read after service discovery + // to avoid peripherals disconnecting due to inactivity + this->update(); } break; } diff --git a/esphome/components/button/automation.h b/esphome/components/button/automation.h index a5fb9f35b70..3b792eb5d72 100644 --- a/esphome/components/button/automation.h +++ b/esphome/components/button/automation.h @@ -11,7 +11,7 @@ template class PressAction : public Action { public: explicit PressAction(Button *button) : button_(button) {} - void play(Ts... x) override { this->button_->press(); } + void play(const Ts &...x) override { this->button_->press(); } protected: Button *button_; diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index e1de1eb2f21..7b51c2c45c1 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -4,7 +4,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID -from esphome.core import CORE +from esphome.core import CORE, ID CODEOWNERS = ["@mvturnho", "@danielschramm"] IS_PLATFORM_COMPONENT = True @@ -176,5 +176,8 @@ async def canbus_action_to_code(config, action_id, template_arg, args): else: if isinstance(data, bytes): data = [int(x) for x in data] - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 51d7c0830aa..f7b84111bd5 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -112,13 +112,16 @@ class Canbus : public Component { template class CanbusSendAction : public Action, public Parented { public: - void set_data_template(const std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } @@ -129,25 +132,30 @@ template class CanbusSendAction : public Action, public P this->remote_transmission_request_ = remote_transmission_request; } - void play(Ts... x) override { + void play(const Ts &...x) override { auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto use_extended_id = this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; - if (this->static_) { - this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_); + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { - auto val = this->data_func_(x...); - this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val); + // Template mode: call function + data = this->data_.func(x...); } + this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data); } protected: optional can_id_{}; optional use_extended_id_{}; bool remote_transmission_request_{false}; - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; class CanbusTrigger : public Trigger, uint32_t, bool>, public Component { diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 99acb76bcf3..9bd3ef8a058 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -1,9 +1,12 @@ +import logging + import esphome.codegen as cg from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( + CONF_AP, CONF_ID, PLATFORM_BK72XX, PLATFORM_ESP32, @@ -14,6 +17,10 @@ from esphome.const import ( ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) def AUTO_LOAD() -> list[str]: @@ -50,6 +57,27 @@ CONFIG_SCHEMA = cv.All( ) +def _final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + wifi_conf = full_config.get("wifi") + + if wifi_conf is None: + # This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway + raise cv.Invalid("Captive portal requires the wifi component to be configured") + + if CONF_AP not in wifi_conf: + _LOGGER.warning( + "Captive portal is enabled but no WiFi AP is configured. " + "The captive portal will not be accessible. " + "Add 'ap:' to your WiFi configuration to enable the captive portal." + ) + + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index a4d13ade58e..36cc8f4f21d 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -22,7 +22,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(std::string, custom_preset) TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->climate_->make_call(); call.set_mode(this->mode_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...)); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index e596582de81..82b75660bae 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -1,4 +1,6 @@ #include "climate.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/macros.h" namespace esphome { @@ -292,14 +294,6 @@ const optional &ClimateCall::get_fan_mode() const { return this- const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } -optional ClimateCall::get_custom_fan_mode() const { - return this->custom_fan_mode_ != nullptr ? std::string(this->custom_fan_mode_) : optional{}; -} - -optional ClimateCall::get_custom_preset() const { - return this->custom_preset_ != nullptr ? std::string(this->custom_preset_) : optional{}; -} - ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { this->target_temperature_high_ = target_temperature_high; return *this; @@ -471,6 +465,9 @@ void Climate::publish_state() { // Send state to frontend this->state_callback_.call(*this); +#if defined(USE_CLIMATE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_climate_update(this); +#endif // Save state this->save_state_(); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 091483a0337..b277877c3e1 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -107,8 +107,10 @@ class ClimateCall { const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_preset() const; - optional get_custom_fan_mode() const; - optional get_custom_preset() const; + const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } + const char *get_custom_preset() const { return this->custom_preset_; } + bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } + bool has_custom_preset() const { return this->custom_preset_ != nullptr; } protected: void validate_(); @@ -122,6 +124,8 @@ class ClimateCall { optional fan_mode_; optional swing_mode_; optional preset_; + + private: const char *custom_fan_mode_{nullptr}; const char *custom_preset_{nullptr}; }; diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h index 3b78e17cf4c..ad089bbe7d7 100644 --- a/esphome/components/cm1106/cm1106.h +++ b/esphome/components/cm1106/cm1106.h @@ -30,7 +30,7 @@ template class CM1106CalibrateZeroAction : public Action public: CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {} - void play(Ts... x) override { this->cm1106_->calibrate_zero(400); } + void play(const Ts &...x) override { this->cm1106_->calibrate_zero(400); } protected: CM1106Component *cm1106_; diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 19924f0da77..12a69551f56 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -8,7 +8,10 @@ BYTE_ORDER_BIG = "big_endian" CONF_COLOR_DEPTH = "color_depth" CONF_DRAW_ROUNDING = "draw_rounding" +CONF_ENABLED = "enabled" +CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_ON_RECEIVE = "on_receive" CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" +CONF_ROWS = "rows" CONF_USE_PSRAM = "use_psram" diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 6406ba52cb9..752e0398c18 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -11,7 +11,7 @@ template class OpenAction : public Action { public: explicit OpenAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_open().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_open().perform(); } protected: Cover *cover_; @@ -21,7 +21,7 @@ template class CloseAction : public Action { public: explicit CloseAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_close().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_close().perform(); } protected: Cover *cover_; @@ -31,7 +31,7 @@ template class StopAction : public Action { public: explicit StopAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_stop().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_stop().perform(); } protected: Cover *cover_; @@ -41,7 +41,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->make_call().set_command_toggle().perform(); } + void play(const Ts &...x) override { this->cover_->make_call().set_command_toggle().perform(); } protected: Cover *cover_; @@ -55,7 +55,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(float, tilt) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->cover_->make_call(); if (this->stop_.has_value()) call.set_stop(this->stop_.value(x...)); @@ -77,7 +77,7 @@ template class CoverPublishAction : public Action { TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(CoverOperation, current_operation) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->position_.has_value()) this->cover_->position = this->position_.value(x...); if (this->tilt_.has_value()) @@ -94,7 +94,7 @@ template class CoverPublishAction : public Action { template class CoverIsOpenCondition : public Condition { public: CoverIsOpenCondition(Cover *cover) : cover_(cover) {} - bool check(Ts... x) override { return this->cover_->is_fully_open(); } + bool check(const Ts &...x) override { return this->cover_->is_fully_open(); } protected: Cover *cover_; @@ -103,7 +103,7 @@ template class CoverIsOpenCondition : public Condition { template class CoverIsClosedCondition : public Condition { public: CoverIsClosedCondition(Cover *cover) : cover_(cover) {} - bool check(Ts... x) override { return this->cover_->is_fully_closed(); } + bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } protected: Cover *cover_; diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 654bb956a5c..3062dba28ad 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -1,5 +1,9 @@ #include "cover.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" + #include + #include "esphome/core/log.h" namespace esphome { @@ -169,6 +173,9 @@ void Cover::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); this->state_callback_.call(); +#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_cover_update(this); +#endif if (save) { CoverRestoreState restore{}; diff --git a/esphome/components/cs5460a/cs5460a.h b/esphome/components/cs5460a/cs5460a.h index 15ae04f3c6f..11b13f5851f 100644 --- a/esphome/components/cs5460a/cs5460a.h +++ b/esphome/components/cs5460a/cs5460a.h @@ -114,7 +114,7 @@ template class CS5460ARestartAction : public Action { public: CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {} - void play(Ts... x) override { cs5460a_->restart(); } + void play(const Ts &...x) override { cs5460a_->restart(); } protected: CS5460AComponent *cs5460a_; diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index a518c964894..a3969e081e5 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -70,7 +70,7 @@ bool DallasTemperatureSensor::read_scratch_pad_() { } void DallasTemperatureSensor::setup() { - if (!this->check_address_()) + if (!this->check_address_or_index_()) return; if (!this->read_scratch_pad_()) return; diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index c164a98b2e3..2c2775ecf40 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -1,5 +1,6 @@ #include "date_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATE #include "esphome/core/log.h" @@ -32,6 +33,9 @@ void DateEntity::publish_state() { this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); this->state_callback_.call(); +#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_date_update(this); +#endif } DateCall DateEntity::make_call() { return DateCall(this); } diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index fcbb46cf176..ba2edb127ad 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -101,7 +101,7 @@ template class DateSetAction : public Action, public Pare public: TEMPLATABLE_VALUE(ESPTime, date) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); if (this->date_.has_value()) { diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 4e3b051eb35..8606a47fa72 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -1,5 +1,6 @@ #include "datetime_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_DATETIME #include "esphome/core/log.h" @@ -48,6 +49,9 @@ void DateTimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_, this->day_, this->hour_, this->minute_, this->second_); this->state_callback_.call(); +#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_datetime_update(this); +#endif } DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 275eedfd3b5..43bff5a1812 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -124,7 +124,7 @@ template class DateTimeSetAction : public Action, public public: TEMPLATABLE_VALUE(ESPTime, datetime) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); if (this->datetime_.has_value()) { diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 9b05c2124f9..469be077eab 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -1,5 +1,6 @@ #include "time_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #ifdef USE_DATETIME_TIME #include "esphome/core/log.h" @@ -29,6 +30,9 @@ void TimeEntity::publish_state() { ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_); this->state_callback_.call(); +#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_time_update(this); +#endif } TimeCall TimeEntity::make_call() { return TimeCall(this); } diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index e79b8c225de..c5cbeb52da1 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -103,7 +103,7 @@ template class TimeSetAction : public Action, public Pare public: TEMPLATABLE_VALUE(ESPTime, time) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); if (this->time_.has_value()) { diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 6b4205a5450..dc032f442e1 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -59,6 +59,7 @@ async def to_code(config): zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + cg.add_define("USE_DEBUG") FILTER_SOURCE_FILES = filter_source_files_from_platform( diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index ade0968e08f..f54bf82eae0 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -49,9 +49,9 @@ void DebugComponent::dump_config() { } #endif // USE_TEXT_SENSOR -#ifdef USE_ESP32 - this->log_partition_info_(); // Log partition information for ESP32 -#endif // USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ZEPHYR) + this->log_partition_info_(); // Log partition information +#endif } void DebugComponent::loop() { diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index efd0dafab0c..96306f7cdfe 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -62,19 +62,19 @@ class DebugComponent : public PollingComponent { sensor::Sensor *cpu_frequency_sensor_{nullptr}; #endif // USE_SENSOR -#ifdef USE_ESP32 +#if defined(USE_ESP32) || defined(USE_ZEPHYR) /** * @brief Logs information about the device's partition table. * - * This function iterates through the ESP32's partition table and logs details + * This function iterates through the partition table and logs details * about each partition, including its name, type, subtype, starting address, * and size. The information is useful for diagnosing issues related to flash * memory or verifying the partition configuration dynamically at runtime. * - * Only available when compiled for ESP32 platforms. + * Only available when compiled for ESP32 and ZEPHYR platforms. */ void log_partition_info_(); -#endif // USE_ESP32 +#endif #ifdef USE_TEXT_SENSOR text_sensor::TextSensor *device_info_{nullptr}; diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 231b39a7111..c888c41a782 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -5,11 +5,11 @@ #include #include #include +#include #define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] -namespace esphome { -namespace debug { +namespace esphome::debug { static const char *const TAG = "debug"; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; @@ -87,6 +87,37 @@ std::string DebugComponent::get_reset_reason_() { uint32_t DebugComponent::get_free_heap_() { return INT_MAX; } +static void fa_cb(const struct flash_area *fa, void *user_data) { +#if CONFIG_FLASH_MAP_LABELS + const char *fa_label = flash_area_label(fa); + + if (fa_label == nullptr) { + fa_label = "-"; + } + ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s %-24.24s 0x%-10x 0x%-12x", (int) fa->fa_id, + sizeof(uintptr_t) * 2, (uintptr_t) fa->fa_dev, fa->fa_dev->name, fa_label, (uint32_t) fa->fa_off, + fa->fa_size); +#else + ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s 0x%-10x 0x%-12x", (int) fa->fa_id, sizeof(uintptr_t) * 2, + (uintptr_t) fa->fa_dev, fa->fa_dev->name, (uint32_t) fa->fa_off, fa->fa_size); +#endif +} + +void DebugComponent::log_partition_info_() { +#if CONFIG_FLASH_MAP_LABELS + ESP_LOGCONFIG(TAG, "ID | Device | Device Name " + "| Label | Offset | Size"); + ESP_LOGCONFIG(TAG, "--------------------------------------------" + "-----------------------------------------------"); +#else + ESP_LOGCONFIG(TAG, "ID | Device | Device Name " + "| Offset | Size"); + ESP_LOGCONFIG(TAG, "-----------------------------------------" + "------------------------------"); +#endif + flash_area_foreach(fa_cb, nullptr); +} + void DebugComponent::get_device_info_(std::string &device_info) { std::string supply = "Main supply status: "; if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) { @@ -281,14 +312,18 @@ void DebugComponent::get_device_info_(std::string &device_info) { NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); + bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] && + (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected + << UICR_PSELRESET_CONNECT_Pos; ESP_LOGD( TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), - YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) != - (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) || - ((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) != - (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)))); - + YESNO(n_reset_enabled)); + if (n_reset_enabled) { + uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; + uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; + ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin); + } #ifdef USE_BOOTLOADER_MCUBOOT ESP_LOGD(TAG, "bootloader: mcuboot"); #else @@ -322,10 +357,22 @@ void DebugComponent::get_device_info_(std::string &device_info) { #endif } #endif + auto uicr = [](volatile uint32_t *data, uint8_t size) { + std::string res; + char buf[sizeof(uint32_t) * 2 + 1]; + for (size_t i = 0; i < size; i++) { + if (i > 0) { + res += ' '; + } + res += format_hex_pretty(data[i], '\0', false); + } + return res; + }; + ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str()); + ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str()); } void DebugComponent::update_platform_() {} -} // namespace debug -} // namespace esphome +} // namespace esphome::debug #endif diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 38744163c79..80381e767c9 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -148,7 +148,7 @@ template class EnterDeepSleepAction : public Action { void set_time(time::RealTimeClock *time) { this->time_ = time; } #endif - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->sleep_duration_.has_value()) { this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); } @@ -207,12 +207,12 @@ template class EnterDeepSleepAction : public Action { template class PreventDeepSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->prevent_deep_sleep(); } + void play(const Ts &...x) override { this->parent_->prevent_deep_sleep(); } }; template class AllowDeepSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->allow_deep_sleep(); } + void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); } }; } // namespace deep_sleep diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h index f8944b07358..e2dfb0142be 100644 --- a/esphome/components/demo/demo_climate.h +++ b/esphome/components/demo/demo_climate.h @@ -63,14 +63,14 @@ class DemoClimate : public climate::Climate, public Component { if (call.get_swing_mode().has_value()) { this->swing_mode = *call.get_swing_mode(); } - if (call.get_custom_fan_mode().has_value()) { - this->set_custom_fan_mode_(call.get_custom_fan_mode()->c_str()); + if (call.has_custom_fan_mode()) { + this->set_custom_fan_mode_(call.get_custom_fan_mode()); } if (call.get_preset().has_value()) { this->set_preset_(*call.get_preset()); } - if (call.get_custom_preset().has_value()) { - this->set_custom_preset_(call.get_custom_preset()->c_str()); + if (call.has_custom_preset()) { + this->set_custom_preset_(call.get_custom_preset()); } this->publish_state(); } diff --git a/esphome/components/demo/demo_select.h b/esphome/components/demo/demo_select.h index 1951a684a2d..1a5df13eda7 100644 --- a/esphome/components/demo/demo_select.h +++ b/esphome/components/demo/demo_select.h @@ -8,7 +8,7 @@ namespace demo { class DemoSelect : public select::Select, public Component { protected: - void control(const std::string &value) override { this->publish_state(value); } + void control(size_t index) override { this->publish_state(index); } }; } // namespace demo diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index d2ec0a2310f..03d2230ca68 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -77,7 +77,7 @@ class DFPlayer : public uart::UARTDevice, public Component { class ACTION_CLASS : /* NOLINT */ \ public Action, \ public Parented { \ - void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ + void play(const Ts &...x) override { this->parent_->ACTION_METHOD(); } \ }; DFPLAYER_SIMPLE_ACTION(NextAction, next) @@ -87,7 +87,7 @@ template class PlayMp3Action : public Action, public Pare public: TEMPLATABLE_VALUE(uint16_t, file) - void play(Ts... x) override { + void play(const Ts &...x) override { auto file = this->file_.value(x...); this->parent_->play_mp3(file); } @@ -98,7 +98,7 @@ template class PlayFileAction : public Action, public Par TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(bool, loop) - void play(Ts... x) override { + void play(const Ts &...x) override { auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); if (loop) { @@ -115,7 +115,7 @@ template class PlayFolderAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(bool, loop) - void play(Ts... x) override { + void play(const Ts &...x) override { auto folder = this->folder_.value(x...); auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); @@ -131,7 +131,7 @@ template class SetDeviceAction : public Action, public Pa public: TEMPLATABLE_VALUE(Device, device) - void play(Ts... x) override { + void play(const Ts &...x) override { auto device = this->device_.value(x...); this->parent_->set_device(device); } @@ -141,7 +141,7 @@ template class SetVolumeAction : public Action, public Pa public: TEMPLATABLE_VALUE(uint8_t, volume) - void play(Ts... x) override { + void play(const Ts &...x) override { auto volume = this->volume_.value(x...); this->parent_->set_volume(volume); } @@ -151,7 +151,7 @@ template class SetEqAction : public Action, public Parent public: TEMPLATABLE_VALUE(EqPreset, eq) - void play(Ts... x) override { + void play(const Ts &...x) override { auto eq = this->eq_.value(x...); this->parent_->set_eq(eq); } @@ -168,7 +168,7 @@ DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down) template class DFPlayerIsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_playing(); } + bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; class DFPlayerFinishedPlaybackTrigger : public Trigger<> { diff --git a/esphome/components/dfrobot_sen0395/automation.h b/esphome/components/dfrobot_sen0395/automation.h index 3f69e482b73..422555d6eba 100644 --- a/esphome/components/dfrobot_sen0395/automation.h +++ b/esphome/components/dfrobot_sen0395/automation.h @@ -11,7 +11,7 @@ namespace dfrobot_sen0395 { template class DfrobotSen0395ResetAction : public Action, public Parented { public: - void play(Ts... x) { this->parent_->enqueue(make_unique()); } + void play(const Ts &...x) { this->parent_->enqueue(make_unique()); } }; template @@ -33,7 +33,7 @@ class DfrobotSen0395SettingsAction : public Action, public Parentedparent_->enqueue(make_unique(0)); if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { this->parent_->enqueue(make_unique()); diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index f2d79d12d95..47d40915aa7 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -176,7 +176,117 @@ class Display; class DisplayPage; class DisplayOnPageChangeTrigger; -using display_writer_t = std::function; +/** Optimized display writer that uses function pointers for stateless lambdas. + * + * Similar to TemplatableValue but specialized for display writer callbacks. + * Saves ~8 bytes per stateless lambda on 32-bit platforms (16 bytes std::function → ~8 bytes discriminator+pointer). + * + * Supports both: + * - Stateless lambdas (from YAML) → function pointer (4 bytes) + * - Stateful lambdas/std::function (from C++ code) → std::function* (heap allocated) + * + * @tparam T The display type (e.g., Display, Nextion, GPIOLCDDisplay) + */ +template class DisplayWriter { + public: + DisplayWriter() : type_(NONE) {} + + // For stateless lambdas (convertible to function pointer): use function pointer (4 bytes) + template + DisplayWriter(F f) requires std::invocable && std::convertible_to + : type_(STATELESS_LAMBDA) { + this->stateless_f_ = f; // Implicit conversion to function pointer + } + + // For stateful lambdas and std::function (not convertible to function pointer): use std::function* (heap allocated) + // This handles backwards compatibility with external components + template + DisplayWriter(F f) requires std::invocable &&(!std::convertible_to) : type_(LAMBDA) { + this->f_ = new std::function(std::move(f)); + } + + // Copy constructor + DisplayWriter(const DisplayWriter &other) : type_(other.type_) { + if (type_ == LAMBDA) { + this->f_ = new std::function(*other.f_); + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; + } + } + + // Move constructor + DisplayWriter(DisplayWriter &&other) noexcept : type_(other.type_) { + if (type_ == LAMBDA) { + this->f_ = other.f_; + other.f_ = nullptr; + } else if (type_ == STATELESS_LAMBDA) { + this->stateless_f_ = other.stateless_f_; + } + other.type_ = NONE; + } + + // Assignment operators + DisplayWriter &operator=(const DisplayWriter &other) { + if (this != &other) { + this->~DisplayWriter(); + new (this) DisplayWriter(other); + } + return *this; + } + + DisplayWriter &operator=(DisplayWriter &&other) noexcept { + if (this != &other) { + this->~DisplayWriter(); + new (this) DisplayWriter(std::move(other)); + } + return *this; + } + + ~DisplayWriter() { + if (type_ == LAMBDA) { + delete this->f_; + } + // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty) + } + + bool has_value() const { return this->type_ != NONE; } + + void call(T &display) const { + switch (this->type_) { + case STATELESS_LAMBDA: + this->stateless_f_(display); // Direct function pointer call + break; + case LAMBDA: + (*this->f_)(display); // std::function call + break; + case NONE: + default: + break; + } + } + + // Operator() for convenience + void operator()(T &display) const { this->call(display); } + + // Operator* for backwards compatibility with (*writer_)(*this) pattern + DisplayWriter &operator*() { return *this; } + const DisplayWriter &operator*() const { return *this; } + + protected: + enum : uint8_t { + NONE, + LAMBDA, + STATELESS_LAMBDA, + } type_; + + union { + std::function *f_; + void (*stateless_f_)(T &); + }; +}; + +// Type alias for Display writer - uses optimized DisplayWriter instead of std::function +using display_writer_t = DisplayWriter; #define LOG_DISPLAY(prefix, type, obj) \ if ((obj) != nullptr) { \ @@ -210,7 +320,7 @@ class Display : public PollingComponent { /// Fill the entire screen with the given color. virtual void fill(Color color); /// Clear the entire screen by filling it with OFF pixels. - void clear(); + virtual void clear(); /// Get the calculated width of the display in pixels with rotation applied. virtual int get_width() { return this->get_width_internal(); } @@ -678,7 +788,7 @@ class Display : public PollingComponent { void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3); DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; - optional writer_{}; + display_writer_t writer_{}; DisplayPage *page_{nullptr}; DisplayPage *previous_page_{nullptr}; std::vector on_page_change_triggers_; @@ -709,7 +819,7 @@ template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) - void play(Ts... x) override { + void play(const Ts &...x) override { auto *page = this->page_.value(x...); if (page != nullptr) { page->show(); @@ -721,7 +831,7 @@ template class DisplayPageShowNextAction : public Action public: DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {} - void play(Ts... x) override { this->buffer_->show_next_page(); } + void play(const Ts &...x) override { this->buffer_->show_next_page(); } Display *buffer_; }; @@ -730,7 +840,7 @@ template class DisplayPageShowPrevAction : public Action public: DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {} - void play(Ts... x) override { this->buffer_->show_prev_page(); } + void play(const Ts &...x) override { this->buffer_->show_prev_page(); } Display *buffer_; }; @@ -740,7 +850,7 @@ template class DisplayIsDisplayingPageCondition : public Conditi DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {} void set_page(DisplayPage *page) { this->page_ = page; } - bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } + bool check(const Ts &...x) override { return this->parent_->get_active_page() == this->page_; } protected: Display *parent_; diff --git a/esphome/components/display_menu_base/automation.h b/esphome/components/display_menu_base/automation.h index d5394a1e0c6..9c64794cefe 100644 --- a/esphome/components/display_menu_base/automation.h +++ b/esphome/components/display_menu_base/automation.h @@ -10,7 +10,7 @@ template class UpAction : public Action { public: explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->up(); } + void play(const Ts &...x) override { this->menu_->up(); } protected: DisplayMenuComponent *menu_; @@ -20,7 +20,7 @@ template class DownAction : public Action { public: explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->down(); } + void play(const Ts &...x) override { this->menu_->down(); } protected: DisplayMenuComponent *menu_; @@ -30,7 +30,7 @@ template class LeftAction : public Action { public: explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->left(); } + void play(const Ts &...x) override { this->menu_->left(); } protected: DisplayMenuComponent *menu_; @@ -40,7 +40,7 @@ template class RightAction : public Action { public: explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->right(); } + void play(const Ts &...x) override { this->menu_->right(); } protected: DisplayMenuComponent *menu_; @@ -50,7 +50,7 @@ template class EnterAction : public Action { public: explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->enter(); } + void play(const Ts &...x) override { this->menu_->enter(); } protected: DisplayMenuComponent *menu_; @@ -60,7 +60,7 @@ template class ShowAction : public Action { public: explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->show(); } + void play(const Ts &...x) override { this->menu_->show(); } protected: DisplayMenuComponent *menu_; @@ -70,7 +70,7 @@ template class HideAction : public Action { public: explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->hide(); } + void play(const Ts &...x) override { this->menu_->hide(); } protected: DisplayMenuComponent *menu_; @@ -80,7 +80,7 @@ template class ShowMainAction : public Action { public: explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {} - void play(Ts... x) override { this->menu_->show_main(); } + void play(const Ts &...x) override { this->menu_->show_main(); } protected: DisplayMenuComponent *menu_; @@ -88,7 +88,7 @@ template class ShowMainAction : public Action { template class IsActiveCondition : public Condition { public: explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {} - bool check(Ts... x) override { return this->menu_->is_active(); } + bool check(const Ts &...x) override { return this->menu_->is_active(); } protected: DisplayMenuComponent *menu_; diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index 077db497b1e..adbd7b5487a 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -23,7 +23,7 @@ void DS1307Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h index 2e9ac2275c2..f7f06253b79 100644 --- a/esphome/components/ds1307/ds1307.h +++ b/esphome/components/ds1307/ds1307.h @@ -59,12 +59,12 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { template class WriteAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->write_time(); } + void play(const Ts &...x) override { this->parent_->write_time(); } }; template class ReadAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->read_time(); } + void play(const Ts &...x) override { this->parent_->read_time(); } }; } // namespace ds1307 } // namespace esphome diff --git a/esphome/components/duty_time/duty_time_sensor.h b/esphome/components/duty_time/duty_time_sensor.h index 18280f8e21e..d9fb2a6d604 100644 --- a/esphome/components/duty_time/duty_time_sensor.h +++ b/esphome/components/duty_time/duty_time_sensor.h @@ -51,15 +51,15 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent { template class BaseAction : public Action, public Parented {}; template class StartAction : public BaseAction { - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopAction : public BaseAction { - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class ResetAction : public BaseAction { - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } }; template class RunningCondition : public Condition, public Parented { @@ -67,7 +67,7 @@ template class RunningCondition : public Condition, publi explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {} protected: - bool check(Ts... x) override { return this->parent_->is_running() == this->state_; } + bool check(const Ts &...x) override { return this->parent_->is_running() == this->state_; } bool state_; }; diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 20549f049dd..9ff393b3975 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -1,21 +1,35 @@ +import importlib +import pkgutil + from esphome import core, pins import esphome.codegen as cg from esphome.components import display, spi +from esphome.components.mipi import flatten_sequence, map_sequence import esphome.config_validation as cv from esphome.const import ( CONF_BUSY_PIN, + CONF_CS_PIN, + CONF_DATA_RATE, CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_HEIGHT, CONF_ID, + CONF_INIT_SEQUENCE, CONF_LAMBDA, CONF_MODEL, - CONF_PAGES, CONF_RESET_DURATION, CONF_RESET_PIN, + CONF_WIDTH, ) +from . import models + AUTO_LOAD = ["split_buffer"] DEPENDENCIES = ["spi"] +CONF_INIT_SEQUENCE_ID = "init_sequence_id" + epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") EPaperBase = epaper_spi_ns.class_( "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer @@ -24,30 +38,79 @@ EPaperBase = epaper_spi_ns.class_( EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) -MODELS = { - "7.3in-spectra-e6": EPaper7p3InSpectraE6, -} +# Import all models dynamically from the models package +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) -CONFIG_SCHEMA = cv.All( - display.FULL_DISPLAY_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(EPaperBase), - cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"), - cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_RESET_DURATION): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(max=core.TimePeriod(milliseconds=500)), - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(spi.spi_device_schema()), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), +MODELS = models.EpaperModel.models + +DIMENSION_SCHEMA = cv.Schema( + { + cv.Required(CONF_WIDTH): cv.int_, + cv.Required(CONF_HEIGHT): cv.int_, + } ) + +def model_schema(config): + model = MODELS[config[CONF_MODEL]] + class_name = epaper_spi_ns.class_(model.class_name, EPaperBase) + cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required + return ( + display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), + ) + ) + .extend( + { + model.option(pin): pins.gpio_output_pin_schema + for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN) + } + ) + .extend( + { + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(class_name), + cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), + cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, + model.option(CONF_ENABLE_PIN): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( + map_sequence + ), + model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } + ) + ) + + +def customise_schema(config): + """ + Create a customised config schema for a specific model and validate the configuration. + :param config: The configuration dictionary to validate + :return: The validated configuration dictionary + :raises cv.Invalid: If the configuration is invalid + """ + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=cv.ALLOW_EXTRA, + )(config) + return model_schema(config)(config) + + +CONFIG_SCHEMA = customise_schema + FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( "epaper_spi", require_miso=False, require_mosi=True ) @@ -56,8 +119,23 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( async def to_code(config): model = MODELS[config[CONF_MODEL]] - rhs = model.new() - var = cg.Pvariable(config[CONF_ID], rhs, model) + init_sequence = config.get(CONF_INIT_SEQUENCE) + if init_sequence is None: + init_sequence = model.get_init_sequence(config) + init_sequence = flatten_sequence(init_sequence) + init_sequence_length = len(init_sequence) + init_sequence_id = cg.static_const_array( + config[CONF_INIT_SEQUENCE_ID], init_sequence + ) + width, height = model.get_dimensions(config) + var = cg.new_Pvariable( + config[CONF_ID], + model.name, + width, + height, + init_sequence_id, + init_sequence_length, + ) await display.register_display(var, config) await spi.register_spi_device(var, config) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index 9630ea7f8b4..cf6a0b0c3d5 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -8,33 +8,20 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi"; -static const LogString *epaper_state_to_string(EPaperState state) { - switch (state) { - case EPaperState::IDLE: - return LOG_STR("IDLE"); - case EPaperState::UPDATE: - return LOG_STR("UPDATE"); - case EPaperState::RESET: - return LOG_STR("RESET"); - case EPaperState::INITIALISE: - return LOG_STR("INITIALISE"); - case EPaperState::TRANSFER_DATA: - return LOG_STR("TRANSFER_DATA"); - case EPaperState::POWER_ON: - return LOG_STR("POWER_ON"); - case EPaperState::REFRESH_SCREEN: - return LOG_STR("REFRESH_SCREEN"); - case EPaperState::POWER_OFF: - return LOG_STR("POWER_OFF"); - case EPaperState::DEEP_SLEEP: - return LOG_STR("DEEP_SLEEP"); - default: - return LOG_STR("UNKNOWN"); - } +static constexpr const char *const EPAPER_STATE_STRINGS[] = { + "IDLE", "UPDATE", "RESET", "RESET_END", + + "SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", +}; + +const char *EPaperBase::epaper_state_to_string_() { + if (auto idx = static_cast(this->state_); idx < std::size(EPAPER_STATE_STRINGS)) + return EPAPER_STATE_STRINGS[idx]; + return "Unknown"; } void EPaperBase::setup() { - if (!this->init_buffer_(this->get_buffer_length())) { + if (!this->init_buffer_(this->buffer_length_)) { this->mark_failed("Failed to initialise buffer"); return; } @@ -50,7 +37,7 @@ bool EPaperBase::init_buffer_(size_t buffer_length) { return true; } -void EPaperBase::setup_pins_() { +void EPaperBase::setup_pins_() const { this->dc_pin_->setup(); // OUTPUT this->dc_pin_->digital_write(false); @@ -81,11 +68,7 @@ void EPaperBase::data(uint8_t value) { // write a command followed by zero or more bytes of data. // The command is the first byte, length is the length of data only in the second byte, followed by the data. // [COMMAND, LENGTH, DATA...] -void EPaperBase::cmd_data(const uint8_t *data) { - const uint8_t command = data[0]; - const uint8_t length = data[1]; - const uint8_t *ptr = data + 2; - +void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, format_hex_pretty(ptr, length, '.', false).c_str()); @@ -99,91 +82,146 @@ void EPaperBase::cmd_data(const uint8_t *data) { this->disable(); } -bool EPaperBase::is_idle_() { +bool EPaperBase::is_idle_() const { if (this->busy_pin_ == nullptr) { return true; } - return this->busy_pin_->digital_read(); + return !this->busy_pin_->digital_read(); } -void EPaperBase::reset() { +bool EPaperBase::reset_() const { if (this->reset_pin_ != nullptr) { - this->reset_pin_->digital_write(false); - this->disable_loop(); - this->set_timeout(this->reset_duration_, [this] { - this->reset_pin_->digital_write(true); - this->set_timeout(20, [this] { this->enable_loop(); }); - }); + if (this->state_ == EPaperState::RESET) { + this->reset_pin_->digital_write(false); + return false; + } + this->reset_pin_->digital_write(true); } + return true; } void EPaperBase::update() { - if (!this->state_queue_.empty()) { - ESP_LOGE(TAG, "Display update already in progress - %s", - LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front()))); + if (this->state_ != EPaperState::IDLE) { + ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); return; } - - this->state_queue_.push(EPaperState::UPDATE); - this->state_queue_.push(EPaperState::RESET); - this->state_queue_.push(EPaperState::INITIALISE); - this->state_queue_.push(EPaperState::TRANSFER_DATA); - this->state_queue_.push(EPaperState::POWER_ON); - this->state_queue_.push(EPaperState::REFRESH_SCREEN); - this->state_queue_.push(EPaperState::POWER_OFF); - this->state_queue_.push(EPaperState::DEEP_SLEEP); - this->state_queue_.push(EPaperState::IDLE); - + this->set_state_(EPaperState::RESET); this->enable_loop(); } +void EPaperBase::wait_for_idle_(bool should_wait) { +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + if (should_wait) { + this->waiting_for_idle_start_ = millis(); + this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_; + } +#endif + this->waiting_for_idle_ = should_wait; +} + +/** + * Called during the loop task. + * First defer for any pending delays, then check if we are waiting for the display to become idle. + * If not waiting for idle, process the state machine. + */ + void EPaperBase::loop() { + auto now = millis(); + if (this->delay_until_ != 0) { + // using modulus arithmetic to handle wrap-around + int diff = now - this->delay_until_; + if (diff < 0) { + return; + } + this->delay_until_ = 0; + } if (this->waiting_for_idle_) { if (this->is_idle_()) { this->waiting_for_idle_ = false; + ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_)); } else { - if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) { - ESP_LOGV(TAG, "Waiting for idle"); - this->waiting_for_idle_last_print_ = App.get_loop_component_start_time(); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + if (now - this->waiting_for_idle_last_print_ >= 1000) { + ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_()); + this->waiting_for_idle_last_print_ = millis(); } +#endif return; } } + this->process_state_(); +} - auto state = this->state_queue_.front(); - - switch (state) { +/** + * Process the state machine. + * Typical state sequence: + * IDLE -> RESET -> RESET_END -> UPDATE -> INITIALISE -> TRANSFER_DATA -> POWER_ON -> REFRESH_SCREEN -> POWER_OFF -> + * DEEP_SLEEP -> IDLE + * + * Should a subclassed class need to override this, the method will need to be made virtual. + */ +void EPaperBase::process_state_() { + ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_()); + switch (this->state_) { + default: + ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_()); + this->disable_loop(); + break; case EPaperState::IDLE: this->disable_loop(); break; + case EPaperState::RESET: + case EPaperState::RESET_END: + if (this->reset_()) { + this->set_state_(EPaperState::UPDATE); + } else { + this->set_state_(EPaperState::RESET_END); + } + break; case EPaperState::UPDATE: this->do_update_(); // Calls ESPHome (current page) lambda - break; - case EPaperState::RESET: - this->reset(); + this->set_state_(EPaperState::INITIALISE); break; case EPaperState::INITIALISE: this->initialise_(); + this->set_state_(EPaperState::TRANSFER_DATA); break; case EPaperState::TRANSFER_DATA: if (!this->transfer_data()) { return; // Not done yet, come back next loop } + this->set_state_(EPaperState::POWER_ON); break; case EPaperState::POWER_ON: this->power_on(); + this->set_state_(EPaperState::REFRESH_SCREEN); break; case EPaperState::REFRESH_SCREEN: this->refresh_screen(); + this->set_state_(EPaperState::POWER_OFF); break; case EPaperState::POWER_OFF: this->power_off(); + this->set_state_(EPaperState::DEEP_SLEEP); break; case EPaperState::DEEP_SLEEP: this->deep_sleep(); + this->set_state_(EPaperState::IDLE); break; } - this->state_queue_.pop(); +} + +void EPaperBase::set_state_(EPaperState state, uint16_t delay) { + ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_()); + this->state_ = state; + this->wait_for_idle_(state > EPaperState::SHOULD_WAIT); + if (delay != 0) { + this->delay_until_ = millis() + delay; + } else { + this->delay_until_ = 0; + } + ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay, + TRUEFALSE(this->waiting_for_idle_)); } void EPaperBase::start_command_() { @@ -203,25 +241,39 @@ void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } void EPaperBase::initialise_() { size_t index = 0; - const auto &sequence = this->init_sequence_; - const size_t sequence_size = this->init_sequence_length_; - while (index != sequence_size) { - if (sequence_size - index < 2) { - this->mark_failed("Malformed init sequence"); - return; - } - const auto *ptr = sequence + index; - const uint8_t length = ptr[1]; - if (sequence_size - index < length + 2) { - this->mark_failed("Malformed init sequence"); - return; - } - this->cmd_data(ptr); - index += length + 2; + auto *sequence = this->init_sequence_; + auto length = this->init_sequence_length_; + while (index != length) { + if (length - index < 2) { + this->mark_failed("Malformed init sequence"); + return; + } + const uint8_t cmd = sequence[index++]; + if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) { + ESP_LOGV(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + const uint8_t num_args = x & 0x7F; + if (length - index < num_args) { + ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args); + this->mark_failed(); + return; + } + ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args); + this->cmd_data(cmd, sequence + index, num_args); + index += num_args; + } } +} - this->power_on(); +void EPaperBase::dump_config() { + LOG_DISPLAY("", "E-Paper SPI", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->name_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); } } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index f6b2d41c656..4745ec7339c 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -8,36 +8,48 @@ #include namespace esphome::epaper_spi { +using namespace display; enum class EPaperState : uint8_t { - IDLE, - UPDATE, - RESET, - INITIALISE, - TRANSFER_DATA, - POWER_ON, - REFRESH_SCREEN, - POWER_OFF, - DEEP_SLEEP, + IDLE, // not doing anything + UPDATE, // update the buffer + RESET, // drive reset low (active) + RESET_END, // drive reset high (inactive) + + SHOULD_WAIT, // states higher than this should wait for the display to be not busy + INITIALISE, // send the init sequence + TRANSFER_DATA, // transfer data to the display + POWER_ON, // power on the display + REFRESH_SCREEN, // send refresh command + POWER_OFF, // power off the display + DEEP_SLEEP, // deep sleep the display }; -static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run +static constexpr uint8_t DELAY_FLAG = 0xFF; -class EPaperBase : public display::DisplayBuffer, +class EPaperBase : public DisplayBuffer, public spi::SPIDevice { public: - EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length) - : init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {} + EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY) + : name_(name), + width_(width), + height_(height), + init_sequence_(init_sequence), + init_sequence_length_(init_sequence_length), + display_type_(display_type) {} void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; 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 dump_config() override; void command(uint8_t value); void data(uint8_t value); - void cmd_data(const uint8_t *data); + void cmd_data(uint8_t command, const uint8_t *ptr, size_t length); void update() override; void loop() override; @@ -46,48 +58,84 @@ class EPaperBase : public display::DisplayBuffer, void on_safe_shutdown() override; + DisplayType get_display_type() override { return this->display_type_; }; + protected: - bool is_idle_(); - void setup_pins_(); - virtual void reset(); + int get_height_internal() override { return this->height_; }; + int get_width_internal() override { return this->width_; }; + void process_state_(); + + const char *epaper_state_to_string_(); + bool is_idle_() const; + void setup_pins_() const; + bool reset_() const; void initialise_(); + void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); virtual int get_width_controller() { return this->get_width_internal(); }; - virtual void deep_sleep() = 0; + + /** + * Methods that must be implemented by concrete classes to control the display + */ /** * Send data to the device via SPI - * @return true if done, false if should be called next loop + * @return true if done, false if it should be called next loop */ virtual bool transfer_data() = 0; + /** + * Refresh the screen after data transfer + */ virtual void refresh_screen() = 0; + /** + * Power the display on + */ virtual void power_on() = 0; + /** + * Power the display off + */ virtual void power_off() = 0; - virtual uint32_t get_buffer_length() = 0; + + /** + * Place the display into deep sleep + */ + virtual void deep_sleep() = 0; + + void set_state_(EPaperState state, uint16_t delay = 0); void start_command_(); void end_command_(); void start_data_(); void end_data_(); - const size_t init_sequence_length_{0}; + // properties initialised in the constructor + const char *name_; + uint16_t width_; + uint16_t height_; + const uint8_t *init_sequence_; + size_t init_sequence_length_; + DisplayType display_type_; - size_t current_data_index_{0}; + size_t buffer_length_{}; + size_t current_data_index_{0}; // used by data transfer to track progress uint32_t reset_duration_{200}; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + uint32_t transfer_start_time_{}; uint32_t waiting_for_idle_last_print_{0}; + uint32_t waiting_for_idle_start_{0}; +#endif - GPIOPin *dc_pin_; - GPIOPin *busy_pin_{nullptr}; - GPIOPin *reset_pin_{nullptr}; - - const uint8_t *init_sequence_{nullptr}; + GPIOPin *dc_pin_{}; + GPIOPin *busy_pin_{}; + GPIOPin *reset_pin_{}; bool waiting_for_idle_{false}; + uint32_t delay_until_{0}; split_buffer::SplitBuffer buffer_; - std::queue state_queue_{{EPaperState::IDLE}}; + EPaperState state_{EPaperState::IDLE}; }; } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp deleted file mode 100644 index f6273b392f3..00000000000 --- a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "epaper_spi_model_7p3in_spectra_e6.h" - -namespace esphome::epaper_spi { - -static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6"; - -void EPaper7p3InSpectraE6::power_on() { - ESP_LOGI(TAG, "Power on"); - this->command(0x04); - this->waiting_for_idle_ = true; -} - -void EPaper7p3InSpectraE6::power_off() { - ESP_LOGI(TAG, "Power off"); - this->command(0x02); - this->data(0x00); - this->waiting_for_idle_ = true; -} - -void EPaper7p3InSpectraE6::refresh_screen() { - ESP_LOGI(TAG, "Refresh"); - this->command(0x12); - this->data(0x00); - this->waiting_for_idle_ = true; -} - -void EPaper7p3InSpectraE6::deep_sleep() { - ESP_LOGI(TAG, "Deep sleep"); - this->command(0x07); - this->data(0xA5); -} - -void EPaper7p3InSpectraE6::dump_config() { - LOG_DISPLAY("", "E-Paper SPI", this); - ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6"); - LOG_PIN(" Reset Pin: ", this->reset_pin_); - LOG_PIN(" DC Pin: ", this->dc_pin_); - LOG_PIN(" Busy Pin: ", this->busy_pin_); - LOG_UPDATE_INTERVAL(this); -} - -} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h deleted file mode 100644 index 6e850085ac3..00000000000 --- a/esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include "epaper_spi_spectra_e6.h" - -namespace esphome::epaper_spi { - -class EPaper7p3InSpectraE6 : public EPaperSpectraE6 { - static constexpr const uint16_t WIDTH = 800; - static constexpr const uint16_t HEIGHT = 480; - // clang-format off - - // Command, data length, data - static constexpr uint8_t INIT_SEQUENCE[] = { - 0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18, - 0x01, 1, 0x3F, - 0x00, 2, 0x5F, 0x69, - 0x03, 4, 0x00, 0x54, 0x00, 0x44, - 0x05, 4, 0x40, 0x1F, 0x1F, 0x2C, - 0x06, 4, 0x6F, 0x1F, 0x17, 0x49, - 0x08, 4, 0x6F, 0x1F, 0x1F, 0x22, - 0x30, 1, 0x03, - 0x50, 1, 0x3F, - 0x60, 2, 0x02, 0x00, - 0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256, - 0x84, 1, 0x01, - 0xE3, 1, 0x2F, - }; - // clang-format on - - public: - EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {} - - void dump_config() override; - - protected: - int get_width_internal() override { return WIDTH; }; - int get_height_internal() override { return HEIGHT; }; - - void refresh_screen() override; - void power_on() override; - void power_off() override; - void deep_sleep() override; -}; - -} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp index dccc691252e..8e4cbdde2a3 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp @@ -1,135 +1,166 @@ #include "epaper_spi_spectra_e6.h" +#include + #include "esphome/core/log.h" namespace esphome::epaper_spi { - static constexpr const char *const TAG = "epaper_spi.6c"; +static constexpr size_t MAX_TRANSFER_SIZE = 128; +static constexpr unsigned char GRAY_THRESHOLD = 50; -static inline uint8_t color_to_hex(Color color) { - if (color.red > 127) { - if (color.green > 170) { - if (color.blue > 127) { - return 0x1; // White - } else { - return 0x2; // Yellow - } - } else { - return 0x3; // Red (or Magenta) - } - } else { - if (color.green > 127) { - if (color.blue > 127) { - return 0x5; // Cyan -> Blue - } else { - return 0x6; // Green - } - } else { - if (color.blue > 127) { - return 0x5; // Blue - } else { - return 0x0; // Black - } +enum E6Color { + BLACK, + WHITE, + YELLOW, + RED, + SKIP_1, + BLUE, + GREEN, + CYAN, + SKIP_2, +}; + +static uint8_t color_to_hex(Color color) { + // --- Step 1: Check for Grayscale (Black or White) --- + // We define "grayscale" as a color where the min and max components + // are close to each other. + unsigned char max_rgb = std::max({color.r, color.g, color.b}); + unsigned char min_rgb = std::min({color.r, color.g, color.b}); + + if ((max_rgb - min_rgb) < GRAY_THRESHOLD) { + // It's a shade of gray. Map to BLACK or WHITE. + // We split the luminance at the halfway point (382 = (255*3)/2) + if ((static_cast(color.r) + color.g + color.b) > 382) { + return WHITE; } + return BLACK; } + // --- Step 2: Check for Primary/Secondary Colors --- + // If it's not gray, it's a color. We check which components are + // "on" (over 128) vs "off". This divides the RGB cube into 8 corners. + bool r_on = (color.r > 128); + bool g_on = (color.g > 128); + bool b_on = (color.b > 128); + + if (r_on && g_on && !b_on) { + return YELLOW; + } + if (r_on && !g_on && !b_on) { + return RED; + } + if (!r_on && g_on && !b_on) { + return GREEN; + } + if (!r_on && !g_on && b_on) { + return BLUE; + } + // Handle "impure" colors (Cyan, Magenta) + if (!r_on && g_on && b_on) { + // Cyan (G+B) -> Closest is Green or Blue. Pick Green. + return GREEN; + } + if (r_on && !g_on) { + // Magenta (R+B) -> Closest is Red or Blue. Pick Red. + return RED; + } + // Handle the remaining corners (White-ish, Black-ish) + if (r_on) { + // All high (but not gray) -> White + return WHITE; + } + // !r_on && !g_on && !b_on + // All low (but not gray) -> Black + return BLACK; +} + +void EPaperSpectraE6::power_on() { + ESP_LOGD(TAG, "Power on"); + this->command(0x04); +} + +void EPaperSpectraE6::power_off() { + ESP_LOGD(TAG, "Power off"); + this->command(0x02); + this->data(0x00); +} + +void EPaperSpectraE6::refresh_screen() { + ESP_LOGD(TAG, "Refresh"); + this->command(0x12); + this->data(0x00); +} + +void EPaperSpectraE6::deep_sleep() { + ESP_LOGD(TAG, "Deep sleep"); + this->command(0x07); + this->data(0xA5); } void EPaperSpectraE6::fill(Color color) { - uint8_t pixel_color; - if (color.is_on()) { - pixel_color = color_to_hex(color); - } else { - pixel_color = 0x1; - } + auto pixel_color = color_to_hex(color); - // We store 8 bitset<3> in 3 bytes - // | byte 1 | byte 2 | byte 3 | - // |aaabbbaa|abbbaaab|bbaaabbb| - uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1; - uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2; - uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0; - - const size_t buffer_length = this->get_buffer_length(); - for (size_t i = 0; i < buffer_length; i += 3) { - this->buffer_[i + 0] = byte_1; - this->buffer_[i + 1] = byte_2; - this->buffer_[i + 2] = byte_3; - } + // We store 2 pixels per byte + this->buffer_.fill(pixel_color + (pixel_color << 4)); } -uint32_t EPaperSpectraE6::get_buffer_length() { - // 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes - return this->get_width_controller() * this->get_height_internal() / 8u * 3u; +void EPaperSpectraE6::clear() { + // clear buffer to white, just like real paper. + this->fill(COLOR_ON); } void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { - if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) + if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) return; - uint8_t pixel_bits = color_to_hex(color); + auto pixel_bits = color_to_hex(color); uint32_t pixel_position = x + y * this->get_width_controller(); - uint32_t first_bit_position = pixel_position * 3; - uint32_t byte_position = first_bit_position / 8u; - uint32_t byte_subposition = first_bit_position % 8u; - - if (byte_subposition <= 5) { - this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) | - (pixel_bits << (5 - byte_subposition)); + uint32_t byte_position = pixel_position / 2; + auto original = this->buffer_[byte_position]; + if ((pixel_position & 1) != 0) { + this->buffer_[byte_position] = (original & 0xF0) | pixel_bits; } else { - this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) | - (pixel_bits >> (byte_subposition - 5)); - - this->buffer_[byte_position + 1] = - (this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) | - (pixel_bits << (13 - byte_subposition)); + this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4); } } bool HOT EPaperSpectraE6::transfer_data() { const uint32_t start_time = App.get_loop_component_start_time(); + const size_t buffer_length = this->buffer_length_; if (this->current_data_index_ == 0) { - ESP_LOGV(TAG, "Sending data"); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + this->transfer_start_time_ = millis(); +#endif + ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis()); this->command(0x10); } - uint8_t bytes_to_send[4]{0}; - const size_t buffer_length = this->get_buffer_length(); - for (size_t i = this->current_data_index_; i < buffer_length; i += 3) { - const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]); - // 8 pixels are stored in 3 bytes - // |aaabbbaa|abbbaaab|bbaaabbb| - // | byte 1 | byte 2 | byte 3 | - bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111); - bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111); - bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111); - bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111); + size_t buf_idx = 0; + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + while (this->current_data_index_ != buffer_length) { + bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; - this->start_data_(); - this->write_array(bytes_to_send, sizeof(bytes_to_send)); - this->end_data_(); + if (buf_idx == sizeof bytes_to_send) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->end_data_(); + ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis()); + buf_idx = 0; - if (millis() - start_time > MAX_TRANSFER_TIME) { - // Let the main loop run and come back next loop - this->current_data_index_ = i + 3; - return false; + if (millis() - start_time > MAX_TRANSFER_TIME) { + // Let the main loop run and come back next loop + return false; + } } } // Finished the entire dataset + if (buf_idx != 0) { + this->start_data_(); + this->write_array(bytes_to_send, buf_idx); + this->end_data_(); + } this->current_data_index_ = 0; + ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_); return true; } - -void EPaperSpectraE6::reset() { - if (this->reset_pin_ != nullptr) { - this->disable_loop(); - this->reset_pin_->digital_write(true); - this->set_timeout(20, [this] { - this->reset_pin_->digital_write(false); - delay(2); - this->reset_pin_->digital_write(true); - this->set_timeout(20, [this] { this->enable_loop(); }); - }); - } -} - } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h index 9f0652f79d9..48356ad74bb 100644 --- a/esphome/components/epaper_spi/epaper_spi_spectra_e6.h +++ b/esphome/components/epaper_spi/epaper_spi_spectra_e6.h @@ -6,18 +6,23 @@ namespace esphome::epaper_spi { class EPaperSpectraE6 : public EPaperBase { public: - EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length) - : EPaperBase(init_sequence, init_sequence_length) {} + EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, + size_t init_sequence_length) + : EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) { + this->buffer_length_ = width * height / 2; // 2 pixels per byte + } - display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } void fill(Color color) override; + void clear() override; protected: + void refresh_screen() override; + void power_on() override; + void power_off() override; + void deep_sleep() override; void draw_absolute_pixel_internal(int x, int y, Color color) override; - uint32_t get_buffer_length() override; bool transfer_data() override; - void reset() override; }; } // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/__init__.py b/esphome/components/epaper_spi/models/__init__.py new file mode 100644 index 00000000000..019eb31d181 --- /dev/null +++ b/esphome/components/epaper_spi/models/__init__.py @@ -0,0 +1,65 @@ +from typing import Any, Self + +import esphome.config_validation as cv +from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH + + +class EpaperModel: + models: dict[str, Self] = {} + + def __init__( + self, + name: str, + class_name: str, + initsequence=None, + **defaults, + ): + name = name.upper() + self.name = name + self.class_name = class_name + self.initsequence = initsequence + self.defaults = defaults + EpaperModel.models[name] = self + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + def get_init_sequence(self, config: dict): + return self.initsequence + + def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required: + if fallback is None and self.get_default(name, None) is None: + return cv.Required(name) + return cv.Optional(name, default=self.get_default(name, fallback)) + + def get_dimensions(self, config) -> tuple[int, int]: + if CONF_DIMENSIONS in config: + # Explicit dimensions, just use as is + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + width = dimensions[CONF_WIDTH] + height = dimensions[CONF_HEIGHT] + else: + (width, height) = dimensions + + else: + # Default dimensions, use model defaults + width = self.get_default(CONF_WIDTH) + height = self.get_default(CONF_HEIGHT) + return width, height + + def extend(self, name, **kwargs) -> "EpaperModel": + """ + Extend the current model with additional parameters or a modified init sequence. + Parameters supplied here will override the defaults of the current model. + if the initsequence is not provided, the current model's initsequence will be used. + If add_init_sequence is provided, it will be appended to the current initsequence. + :param name: + :param kwargs: + :return: + """ + initsequence = list(kwargs.pop("initsequence", self.initsequence) or ()) + initsequence.extend(kwargs.pop("add_init_sequence", ())) + defaults = self.defaults.copy() + defaults.update(kwargs) + return self.__class__(name, initsequence=tuple(initsequence), **defaults) diff --git a/esphome/components/epaper_spi/models/spectra_e6.py b/esphome/components/epaper_spi/models/spectra_e6.py new file mode 100644 index 00000000000..9f0b673d69a --- /dev/null +++ b/esphome/components/epaper_spi/models/spectra_e6.py @@ -0,0 +1,51 @@ +from typing import Any + +from . import EpaperModel + + +class SpectraE6(EpaperModel): + def __init__(self, name, class_name="EPaperSpectraE6", **kwargs): + super().__init__(name, class_name, **kwargs) + + # fmt: off + def get_init_sequence(self, config: dict): + width, height = self.get_dimensions(config) + return ( + (0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,), + (0x01, 0x3F,), + (0x00, 0x5F, 0x69,), + (0x03, 0x00, 0x54, 0x00, 0x44,), + (0x05, 0x40, 0x1F, 0x1F, 0x2C,), + (0x06, 0x6F, 0x1F, 0x17, 0x49,), + (0x08, 0x6F, 0x1F, 0x1F, 0x22,), + (0x30, 0x03,), + (0x50, 0x3F,), + (0x60, 0x02, 0x00,), + (0x61, width // 256, width % 256, height // 256, height % 256,), + (0x84, 0x01,), + (0xE3, 0x2F,), + ) + + def get_default(self, key, fallback: Any = False) -> Any: + return self.defaults.get(key, fallback) + + +spectra_e6 = SpectraE6("spectra-e6") + +spectra_e6.extend( + "Seeed-reTerminal-E1002", + width=800, + height=480, + data_rate="20MHz", + cs_pin=10, + dc_pin=11, + reset_pin=12, + busy_pin={ + "number": 13, + "inverted": True, + "mode": { + "input": True, + "pullup": True, + }, + }, +) diff --git a/esphome/components/es8388/select/adc_input_mic_select.cpp b/esphome/components/es8388/select/adc_input_mic_select.cpp index 5fab5b8a92f..2e475342961 100644 --- a/esphome/components/es8388/select/adc_input_mic_select.cpp +++ b/esphome/components/es8388/select/adc_input_mic_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace es8388 { -void ADCInputMicSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_adc_input_mic(static_cast(this->index_of(value).value())); +void ADCInputMicSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_adc_input_mic(static_cast(index)); } } // namespace es8388 diff --git a/esphome/components/es8388/select/adc_input_mic_select.h b/esphome/components/es8388/select/adc_input_mic_select.h index 8d035525ef5..f0fa840d006 100644 --- a/esphome/components/es8388/select/adc_input_mic_select.h +++ b/esphome/components/es8388/select/adc_input_mic_select.h @@ -8,7 +8,7 @@ namespace es8388 { class ADCInputMicSelect : public select::Select, public Parented { protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace es8388 diff --git a/esphome/components/es8388/select/dac_output_select.cpp b/esphome/components/es8388/select/dac_output_select.cpp index 268b5f290cb..9af288a721d 100644 --- a/esphome/components/es8388/select/dac_output_select.cpp +++ b/esphome/components/es8388/select/dac_output_select.cpp @@ -3,9 +3,9 @@ namespace esphome { namespace es8388 { -void DacOutputSelect::control(const std::string &value) { - this->publish_state(value); - this->parent_->set_dac_output(static_cast(this->index_of(value).value())); +void DacOutputSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_dac_output(static_cast(index)); } } // namespace es8388 diff --git a/esphome/components/es8388/select/dac_output_select.h b/esphome/components/es8388/select/dac_output_select.h index fccae9fc191..40d8a665535 100644 --- a/esphome/components/es8388/select/dac_output_select.h +++ b/esphome/components/es8388/select/dac_output_select.h @@ -8,7 +8,7 @@ namespace es8388 { class DacOutputSelect : public select::Select, public Parented { protected: - void control(const std::string &value) override; + void control(size_t index) override; }; } // namespace es8388 diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index ddb8dbb1f0d..0f85e585f7e 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -334,12 +334,14 @@ def _is_framework_url(source: str) -> str: # - https://github.com/espressif/arduino-esp32/releases ARDUINO_FRAMEWORK_VERSION_LOOKUP = { "recommended": cv.Version(3, 3, 2), - "latest": cv.Version(3, 3, 2), - "dev": cv.Version(3, 3, 2), + "latest": cv.Version(3, 3, 4), + "dev": cv.Version(3, 3, 4), } ARDUINO_PLATFORM_VERSION_LOOKUP = { - cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"), - cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"), + cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"), + cv.Version(3, 3, 1): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"), cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"), cv.Version(3, 2, 0): cv.Version(54, 3, 20), @@ -357,8 +359,8 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { "dev": cv.Version(5, 5, 1), } ESP_IDF_PLATFORM_VERSION_LOOKUP = { - cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"), - cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"), + cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), + cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 4, 3): cv.Version(55, 3, 32), cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), @@ -373,14 +375,15 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { # The platform-espressif32 version # - https://github.com/pioarduino/platform-espressif32/releases PLATFORM_VERSION_LOOKUP = { - "recommended": cv.Version(55, 3, 31, "1"), - "latest": cv.Version(55, 3, 31, "1"), - "dev": cv.Version(55, 3, 31, "1"), + "recommended": cv.Version(55, 3, 31, "2"), + "latest": cv.Version(55, 3, 31, "2"), + "dev": cv.Version(55, 3, 31, "2"), } -def _check_versions(value): - value = value.copy() +def _check_versions(config): + config = config.copy() + value = config[CONF_FRAMEWORK] if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP: if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value: @@ -445,7 +448,7 @@ def _check_versions(value): "If there are connectivity or build issues please remove the manual version." ) - return value + return config def _parse_platform_version(value): @@ -495,6 +498,8 @@ def final_validate(config): from esphome.components.psram import DOMAIN as PSRAM_DOMAIN errs = [] + conf_fw = config[CONF_FRAMEWORK] + advanced = conf_fw[CONF_ADVANCED] full_config = fv.full_config.get() if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS): pio_flash_size_key = "board_upload.flash_size" @@ -511,22 +516,14 @@ def final_validate(config): f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" ) ) - if ( - config[CONF_VARIANT] != VARIANT_ESP32 - and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) - and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] - ): + if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_IGNORE_EFUSE_MAC_CRC]: errs.append( cv.Invalid( f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}", path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC], ) ) - if ( - config.get(CONF_FRAMEWORK, {}) - .get(CONF_ADVANCED, {}) - .get(CONF_EXECUTE_FROM_PSRAM) - ): + if advanced[CONF_EXECUTE_FROM_PSRAM]: if config[CONF_VARIANT] != VARIANT_ESP32S3: errs.append( cv.Invalid( @@ -542,6 +539,17 @@ def final_validate(config): ) ) + if ( + config[CONF_FLASH_SIZE] == "32MB" + and "ota" in full_config + and not advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES] + ): + errs.append( + cv.Invalid( + f"OTA with 32MB flash requires '{CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES}' to be set in the '{CONF_ADVANCED}' section of the esp32 configuration", + path=[CONF_FLASH_SIZE], + ) + ) if errs: raise cv.MultipleInvalid(errs) @@ -558,6 +566,7 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" +CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" # VFS requirement tracking # Components that need VFS features can call require_vfs_select() or require_vfs_dir() @@ -595,86 +604,74 @@ def _validate_idf_component(config: ConfigType) -> ConfigType: FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ARDUINO = "arduino" -FRAMEWORK_SCHEMA = cv.All( - cv.Schema( - { - cv.Optional(CONF_TYPE, default=FRAMEWORK_ARDUINO): cv.one_of( - FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO - ), - cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_RELEASE): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, - cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { - cv.string_strict: cv.string_strict - }, - cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( - *LOG_LEVELS_IDF, upper=True - ), - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( - { - cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( - *ASSERTION_LEVELS, upper=True - ), - cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( - *COMPILER_OPTIMIZATIONS, upper=True - ), - cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean, - cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, - cv.Optional( - CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False - ): cv.boolean, - cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean, - # DHCP server is needed for WiFi AP mode. When WiFi component is used, - # it will handle disabling DHCP server when AP is not configured. - # Default to false (disabled) when WiFi is not used. - cv.OnlyWithout( - CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_MDNS_QUERIES, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True - ): cv.boolean, - cv.Optional( - CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True - ): cv.boolean, - cv.Optional( - CONF_DISABLE_VFS_SUPPORT_SELECT, default=True - ): cv.boolean, - cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, - cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, - } - ), - cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( - cv.All( - cv.Schema( - { - cv.Required(CONF_NAME): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.git_ref, - cv.Optional(CONF_REF): cv.string, - cv.Optional(CONF_PATH): cv.string, - cv.Optional(CONF_REFRESH): cv.All( - cv.string, cv.source_refresh - ), - } - ), - _validate_idf_component, - ) - ), - } - ), - _check_versions, +FRAMEWORK_SCHEMA = cv.Schema( + { + cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO), + cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, + cv.Optional(CONF_RELEASE): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { + cv.string_strict: cv.string_strict + }, + cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( + *LOG_LEVELS_IDF, upper=True + ), + cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + { + cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( + *ASSERTION_LEVELS, upper=True + ), + cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of( + *COMPILER_OPTIMIZATIONS, upper=True + ), + cv.Optional( + CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, default=False + ): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean, + cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, + # DHCP server is needed for WiFi AP mode. When WiFi component is used, + # it will handle disabling DHCP server when AP is not configured. + # Default to false (disabled) when WiFi is not used. + cv.OnlyWithout( + CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False + ): cv.boolean, + cv.Optional(CONF_ENABLE_LWIP_MDNS_QUERIES, default=True): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True + ): cv.boolean, + cv.Optional( + CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True + ): cv.boolean, + cv.Optional(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, + cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( + min=8192, max=32768 + ), + } + ), + cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( + cv.All( + cv.Schema( + { + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_SOURCE): cv.git_ref, + cv.Optional(CONF_REF): cv.string, + cv.Optional(CONF_PATH): cv.string, + cv.Optional(CONF_REFRESH): cv.All(cv.string, cv.source_refresh), + } + ), + _validate_idf_component, + ) + ), + } ) @@ -737,11 +734,11 @@ def _show_framework_migration_message(name: str, variant: str) -> None: def _set_default_framework(config): + config = config.copy() if CONF_FRAMEWORK not in config: - config = config.copy() - - variant = config[CONF_VARIANT] config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({}) + if CONF_TYPE not in config[CONF_FRAMEWORK]: + variant = config[CONF_VARIANT] if variant in ARDUINO_ALLOWED_VARIANTS: config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO _show_framework_migration_message( @@ -781,6 +778,7 @@ CONFIG_SCHEMA = cv.All( ), _detect_variant, _set_default_framework, + _check_versions, set_core_data, cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT), ) @@ -799,9 +797,7 @@ def _configure_lwip_max_sockets(conf: dict) -> None: from esphome.components.socket import KEY_SOCKET_CONSUMERS # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS - user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get( - "CONFIG_LWIP_MAX_SOCKETS" - ) + user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS") socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {}) total_sockets = sum(socket_consumers.values()) @@ -926,6 +922,10 @@ async def to_code(config): f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" ), ) + add_idf_sdkconfig_option( + "CONFIG_ARDUINO_LOOP_STACK_SIZE", + conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE], + ) add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) @@ -967,23 +967,18 @@ async def to_code(config): # WiFi component handles its own optimization when AP mode is not used # When using Arduino with Ethernet, DHCP server functions must be available # for the Network library to compile, even if not actively used - if ( - CONF_ENABLE_LWIP_DHCP_SERVER in advanced - and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] - and not ( - conf[CONF_TYPE] == FRAMEWORK_ARDUINO - and "ethernet" in CORE.loaded_integrations - ) + if advanced.get(CONF_ENABLE_LWIP_DHCP_SERVER) is False and not ( + conf[CONF_TYPE] == FRAMEWORK_ARDUINO and "ethernet" in CORE.loaded_integrations ): add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) - if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): + if not advanced[CONF_ENABLE_LWIP_MDNS_QUERIES]: add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) - if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): + if not advanced[CONF_ENABLE_LWIP_BRIDGE_INTERFACE]: add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) _configure_lwip_max_sockets(conf) - if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): + if advanced[CONF_EXECUTE_FROM_PSRAM]: add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) @@ -994,23 +989,22 @@ async def to_code(config): # - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default) # - Up to 200% slower under load when all operations queue through tcpip_thread # Enabling this makes ESP-IDF socket performance match Arduino framework. - if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True): + if advanced[CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING]: add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True) - if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True): + if advanced[CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY]: add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True) # Disable placing libc locks in IRAM to save RAM # This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled) # use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM. - if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): + if advanced[CONF_DISABLE_LIBC_LOCKS_IN_IRAM]: add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) # Disable VFS support for termios (terminal I/O functions) # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). # Saves approximately 1.8KB of flash when disabled (default). add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_TERMIOS", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True), + "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] ) # Disable VFS support for select() with file descriptors @@ -1024,8 +1018,7 @@ async def to_code(config): else: # No component needs it - allow user to control (default: disabled) add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_SELECT", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True), + "CONFIG_VFS_SUPPORT_SELECT", not advanced[CONF_DISABLE_VFS_SUPPORT_SELECT] ) # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.) @@ -1038,8 +1031,7 @@ async def to_code(config): else: # No component needs it - allow user to control (default: disabled) add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_DIR", - not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True), + "CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR] ) cg.add_platformio_option("board_build.partitions", "partitions.csv") @@ -1053,7 +1045,7 @@ async def to_code(config): add_idf_sdkconfig_option(flag, assertion_level == key) add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False) - compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION) + compiler_optimization = advanced[CONF_COMPILER_OPTIMIZATION] for key, flag in COMPILER_OPTIMIZATIONS.items(): add_idf_sdkconfig_option(flag, compiler_optimization == key) @@ -1062,14 +1054,20 @@ async def to_code(config): conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT], ) - if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC): + if advanced[CONF_IGNORE_EFUSE_MAC_CRC]: add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True) add_idf_sdkconfig_option("CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False) - if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): + if advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES]: _LOGGER.warning( "Using experimental features in ESP-IDF may result in unexpected failures." ) add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) + if config[CONF_FLASH_SIZE] == "32MB": + add_idf_sdkconfig_option( + "CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True + ) + + cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define( "USE_ESP_IDF_VERSION_CODE", diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 1c8f29fa950..6215ff862f8 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -1,5 +1,6 @@ #ifdef USE_ESP32 +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -97,9 +98,9 @@ void loop_task(void *pv_params) { extern "C" void app_main() { esp32::setup_preferences(); #if CONFIG_FREERTOS_UNICORE - xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle); + xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle); #else - xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1); + xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1); #endif } #endif // USE_ESP_IDF diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 1ae8df6f5ef..ced7e3fec9b 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -22,6 +22,7 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority import esphome.final_validate as fv DEPENDENCIES = ["esp32"] +AUTO_LOAD = ["socket"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] DOMAIN = "esp32_ble" @@ -482,13 +483,10 @@ async def to_code(config): cg.add(var.set_name(name)) await cg.register_component(var, config) - # BLE uses 1 UDP socket for event notification to wake up main loop from select() + # 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 socket is created in ble_setup_() and used to - # wake lwip_select() when BLE events arrive from the BLE thread. - # Note: Called during config generation, socket is created at runtime. In practice, - # always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT. - socket.consume_sockets(1, "esp32_ble")(config) + # 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) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index b503d4377cc..2f2f5a36eaf 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -27,10 +27,6 @@ extern "C" { #include #endif -#ifdef USE_SOCKET_SELECT_SUPPORT -#include -#endif - namespace esphome::esp32_ble { static const char *const TAG = "esp32_ble"; @@ -301,21 +297,10 @@ bool ESP32BLE::ble_setup_() { // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT - // Set up notification socket to wake main loop for BLE events - // This enables low-latency (~12μs) event processing instead of waiting for select() timeout -#ifdef USE_SOCKET_SELECT_SUPPORT - this->setup_event_notification_(); -#endif - return true; } bool ESP32BLE::ble_dismantle_() { - // Clean up notification socket first before dismantling BLE stack -#ifdef USE_SOCKET_SELECT_SUPPORT - this->cleanup_event_notification_(); -#endif - esp_err_t err = esp_bluedroid_disable(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); @@ -413,12 +398,6 @@ void ESP32BLE::loop() { break; } -#ifdef USE_SOCKET_SELECT_SUPPORT - // Drain any notification socket events first - // This clears the socket so it doesn't stay "ready" in subsequent select() calls - this->drain_event_notifications_(); -#endif - BLEEvent *ble_event = this->ble_events_.pop(); while (ble_event != nullptr) { switch (ble_event->type_) { @@ -585,9 +564,17 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa GAP_ADV_COMPLETE_EVENTS: // Connection events - used by ble_client case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: + enqueue_ble_event(event, param); + return; + // Security events - used by ble_client and bluetooth_proxy + // These are rare but interactive (pairing/bonding), so notify immediately 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 @@ -608,8 +595,8 @@ 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 -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); #endif } #endif @@ -619,8 +606,8 @@ 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 -#ifdef USE_SOCKET_SELECT_SUPPORT - global_ble->notify_main_loop_(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + App.wake_loop_threadsafe(); #endif } #endif @@ -651,99 +638,18 @@ void ESP32BLE::dump_config() { io_capability_s = "invalid"; break; } + char mac_s[18]; + format_mac_addr_upper(mac_address, mac_s); ESP_LOGCONFIG(TAG, "BLE:\n" " MAC address: %s\n" " IO Capability: %s", - format_mac_address_pretty(mac_address).c_str(), io_capability_s); + mac_s, io_capability_s); } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } } -#ifdef USE_SOCKET_SELECT_SUPPORT -void ESP32BLE::setup_event_notification_() { - // Create UDP socket for event notifications - this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (this->notify_fd_ < 0) { - ESP_LOGW(TAG, "Event socket create failed: %d", errno); - return; - } - - // 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_port = 0; // Auto-assign port - - if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - ESP_LOGW(TAG, "Event socket bind failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Get the assigned address and connect to it - // Connecting a UDP socket allows using send() instead of sendto() for better performance - struct sockaddr_in notify_addr; - socklen_t len = sizeof(notify_addr); - if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) { - ESP_LOGW(TAG, "Event socket address failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Connect to self (loopback) - allows using send() instead of sendto() - // After connect(), no need to store notify_addr - the socket remembers it - if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) { - ESP_LOGW(TAG, "Event socket connect failed: %d", errno); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - // Set non-blocking mode - int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0); - lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK); - - // Register with application's select() loop - if (!App.register_socket_fd(this->notify_fd_)) { - ESP_LOGW(TAG, "Event socket register failed"); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - return; - } - - ESP_LOGD(TAG, "Event socket ready"); -} - -void ESP32BLE::cleanup_event_notification_() { - if (this->notify_fd_ >= 0) { - App.unregister_socket_fd(this->notify_fd_); - lwip_close(this->notify_fd_); - this->notify_fd_ = -1; - ESP_LOGD(TAG, "Event socket closed"); - } -} - -void ESP32BLE::drain_event_notifications_() { - // Called from main loop to drain any pending notifications - // Must check is_socket_ready() to avoid blocking on empty socket - if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) { - char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE]; - // Drain all pending notifications with non-blocking reads - // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK - // We control both ends of this loopback socket (always write 1 byte per event), - // so no error checking needed - any errors indicate catastrophic system failure - while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) { - // Just draining, no action needed - actual BLE events are already queued - } - } -} - -#endif // USE_SOCKET_SELECT_SUPPORT - uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { uint64_t u = 0; u |= uint64_t(address[0] & 0xFF) << 40; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index e77453a0205..a2c2b00d142 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -25,10 +25,6 @@ #include #include -#ifdef USE_SOCKET_SELECT_SUPPORT -#include -#endif - namespace esphome::esp32_ble { // Maximum size of the BLE event queue @@ -167,12 +163,10 @@ class ESP32BLE : public Component { void advertising_init_(); #endif -#ifdef USE_SOCKET_SELECT_SUPPORT - void setup_event_notification_(); // Create notification socket - void cleanup_event_notification_(); // Close and unregister socket - inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined) - void drain_event_notifications_(); // Read pending notifications in main loop -#endif + // BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop + // from BLE tasks. This enables low-latency (~12μs) event processing instead of + // waiting for select() timeout (0-16ms). The wake socket is shared with other + // components that need this functionality. private: template friend void enqueue_ble_event(Args... args); @@ -208,13 +202,6 @@ class ESP32BLE : public Component { esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) uint32_t advertising_cycle_time_{}; // 4 bytes -#ifdef USE_SOCKET_SELECT_SUPPORT - // Event notification socket for waking up main loop from BLE thread - // Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout - // Socket is connected during setup, allowing use of send() instead of sendto() for efficiency - int notify_fd_{-1}; // 4 bytes (file descriptor) -#endif - // 2-byte aligned members uint16_t appearance_{0}; // 2 bytes @@ -226,42 +213,19 @@ class ESP32BLE : public Component { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32BLE *global_ble; -#ifdef USE_SOCKET_SELECT_SUPPORT -// Inline implementations for hot-path functions -// These are called from BLE thread (notify) and main loop (drain) on every event - -// Small buffer for draining notification bytes (1 byte sent per BLE event) -// Size allows draining multiple notifications per recvfrom() without wasting stack -static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16; - -inline void ESP32BLE::notify_main_loop_() { - // Called from BLE thread context when events are queued - // Wakes up lwip_select() in main loop by writing to connected loopback socket - if (this->notify_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, and the - // BLE event is already queued. Notification is best-effort to reduce latency. - // This is safe to call from BLE thread - send() is thread-safe in lwip - // Socket is already connected to loopback address, so send() is faster than sendto() - lwip_send(this->notify_fd_, &dummy, 1, 0); - } -} -#endif // USE_SOCKET_SELECT_SUPPORT - template class BLEEnabledCondition : public Condition { public: - bool check(Ts... x) override { return global_ble->is_active(); } + bool check(const Ts &...x) override { return global_ble->is_active(); } }; template class BLEEnableAction : public Action { public: - void play(Ts... x) override { global_ble->enable(); } + void play(const Ts &...x) override { global_ble->enable(); } }; template class BLEDisableAction : public Action { public: - void play(Ts... x) override { global_ble->disable(); } + void play(const Ts &...x) override { global_ble->disable(); } }; } // namespace esphome::esp32_ble diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index 543b1153fce..fe186002803 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -71,7 +71,7 @@ template class BLECharacteristicSetValueAction : public Action, buffer) void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } - void play(Ts... x) override { + void play(const Ts &...x) override { // If the listener is already set, do nothing if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_)) return; @@ -96,7 +96,7 @@ template class BLECharacteristicSetValueAction : public Action class BLECharacteristicNotifyAction : public Action { public: BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {} - void play(Ts... x) override { + void play(const Ts &...x) override { #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION // Call the pre-notify event BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_); @@ -116,7 +116,7 @@ template class BLEDescriptorSetValueAction : public Action, buffer) void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } - void play(Ts... x) override { this->parent_->set_value(this->buffer_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } protected: BLEDescriptor *parent_; diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 784f2eaaa23..bbf7992fa45 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -10,7 +10,7 @@ namespace esphome::esp32_ble_tracker { class ESPBTAdvertiseTrigger : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_addresses(const std::vector &addresses) { this->address_vec_ = addresses; } + void set_addresses(std::initializer_list addresses) { this->address_vec_ = addresses; } bool parse_device(const ESPBTDevice &device) override { uint64_t u64_addr = device.address_uint64(); @@ -96,7 +96,7 @@ template class ESP32BLEStartScanAction : public Action { public: ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {} TEMPLATABLE_VALUE(bool, continuous) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_scan_continuous(this->continuous_.value(x...)); this->parent_->start_scan(); } @@ -107,7 +107,7 @@ template class ESP32BLEStartScanAction : public Action { template class ESP32BLEStopScanAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop_scan(); } + void play(const Ts &...x) override { this->parent_->stop_scan(); } }; } // namespace esphome::esp32_ble_tracker diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index d8ba098645e..d9d9bc0a56c 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -4,6 +4,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import i2c from esphome.components.esp32 import add_idf_component +from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv from esphome.const import ( CONF_BRIGHTNESS, @@ -26,10 +27,9 @@ import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) +AUTO_LOAD = ["camera"] DEPENDENCIES = ["esp32"] -AUTO_LOAD = ["camera", "psram"] - esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") @@ -163,6 +163,14 @@ CONF_ON_IMAGE = "on_image" camera_range_param = cv.int_range(min=-2, max=2) + +def validate_fb_location_(value): + validator = cv.enum(ENUM_FB_LOCATION, upper=True) + if value.lower() == psram_domain: + validator = cv.All(validator, cv.requires_component(psram_domain)) + return validator(value) + + CONFIG_SCHEMA = cv.All( cv.ENTITY_BASE_SCHEMA.extend( { @@ -236,9 +244,9 @@ CONFIG_SCHEMA = cv.All( cv.framerate, cv.Range(min=0, max=1) ), cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), - cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum( - ENUM_FB_LOCATION, upper=True - ), + cv.Optional( + CONF_FRAME_BUFFER_LOCATION, default="PSRAM" + ): validate_fb_location_, cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( diff --git a/esphome/components/esp32_hosted/update/__init__.py b/esphome/components/esp32_hosted/update/__init__.py new file mode 100644 index 00000000000..040f989a646 --- /dev/null +++ b/esphome/components/esp32_hosted/update/__init__.py @@ -0,0 +1,78 @@ +import hashlib +from typing import Any + +import esphome.codegen as cg +from esphome.components import esp32, update +import esphome.config_validation as cv +from esphome.const import CONF_PATH, CONF_RAW_DATA_ID +from esphome.core import CORE, HexInt + +CODEOWNERS = ["@swoboda1337"] +AUTO_LOAD = ["sha256", "watchdog"] +DEPENDENCIES = ["esp32_hosted"] + +CONF_SHA256 = "sha256" + +esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted") +Esp32HostedUpdate = esp32_hosted_ns.class_( + "Esp32HostedUpdate", update.UpdateEntity, cg.Component +) + + +def _validate_sha256(value: Any) -> str: + value = cv.string_strict(value) + if len(value) != 64: + raise cv.Invalid("SHA256 must be 64 hexadecimal characters") + try: + bytes.fromhex(value) + except ValueError as e: + raise cv.Invalid(f"SHA256 must be valid hexadecimal: {e}") from e + return value + + +CONFIG_SCHEMA = cv.All( + update.update_schema(Esp32HostedUpdate, device_class="firmware").extend( + { + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), + cv.Required(CONF_PATH): cv.file_, + cv.Required(CONF_SHA256): _validate_sha256, + } + ), + esp32.only_on_variant( + supported=[ + esp32.const.VARIANT_ESP32H2, + esp32.const.VARIANT_ESP32P4, + ] + ), +) + + +def _validate_firmware(config: dict[str, Any]) -> None: + path = CORE.relative_config_path(config[CONF_PATH]) + with open(path, "rb") as f: + firmware_data = f.read() + calculated = hashlib.sha256(firmware_data).hexdigest() + expected = config[CONF_SHA256].lower() + if calculated != expected: + raise cv.Invalid( + f"SHA256 mismatch for {config[CONF_PATH]}: expected {expected}, got {calculated}" + ) + + +FINAL_VALIDATE_SCHEMA = _validate_firmware + + +async def to_code(config: dict[str, Any]) -> None: + var = await update.new_update(config) + + path = config[CONF_PATH] + with open(CORE.relative_config_path(path), "rb") as f: + firmware_data = f.read() + rhs = [HexInt(x) for x in firmware_data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + + sha256_bytes = bytes.fromhex(config[CONF_SHA256]) + cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes])) + cg.add(var.set_firmware_data(prog_arr)) + cg.add(var.set_firmware_size(len(firmware_data))) + await cg.register_component(var, config) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp new file mode 100644 index 00000000000..adbcc5bf115 --- /dev/null +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -0,0 +1,164 @@ +#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) +#include "esp32_hosted_update.h" +#include "esphome/components/watchdog/watchdog.h" +#include "esphome/components/sha256/sha256.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include +#include +#include + +extern "C" { +#include +} + +namespace esphome::esp32_hosted { + +static const char *const TAG = "esp32_hosted.update"; + +// older coprocessor firmware versions have a 1500-byte limit per RPC call +constexpr size_t CHUNK_SIZE = 1500; + +void Esp32HostedUpdate::setup() { + this->update_info_.title = "ESP32 Hosted Coprocessor"; + + // get coprocessor version + esp_hosted_coprocessor_fwver_t ver_info; + if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { + this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); + } else { + this->update_info_.current_version = "unknown"; + } + ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str()); + + // get image version + const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t); + if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) { + esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset); + if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) { + ESP_LOGD(TAG, "Firmware version: %s", app_desc->version); + ESP_LOGD(TAG, "Project name: %s", app_desc->project_name); + ESP_LOGD(TAG, "Build date: %s", app_desc->date); + ESP_LOGD(TAG, "Build time: %s", app_desc->time); + ESP_LOGD(TAG, "IDF version: %s", app_desc->idf_ver); + this->update_info_.latest_version = app_desc->version; + if (this->update_info_.latest_version != this->update_info_.current_version) { + this->state_ = update::UPDATE_STATE_AVAILABLE; + } else { + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + } else { + ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word, + ESP_APP_DESC_MAGIC_WORD); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + } else { + ESP_LOGW(TAG, "Firmware too small to contain app description"); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + } + + // publish state + this->status_clear_error(); + this->publish_state(); +} + +void Esp32HostedUpdate::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 Hosted Update:\n" + " Current Version: %s\n" + " Latest Version: %s\n" + " Latest Size: %zu bytes", + this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(), + this->firmware_size_); +} + +void Esp32HostedUpdate::perform(bool force) { + if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) { + ESP_LOGW(TAG, "Update not available"); + return; + } + + if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) { + ESP_LOGE(TAG, "No firmware data available"); + return; + } + + sha256::SHA256 hasher; + hasher.init(); + hasher.add(this->firmware_data_, this->firmware_size_); + hasher.calculate(); + if (!hasher.equals_bytes(this->firmware_sha256_.data())) { + this->status_set_error("SHA256 verification failed"); + this->publish_state(); + return; + } + + ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_); + + watchdog::WatchdogManager watchdog(20000); + update::UpdateState prev_state = this->state_; + this->state_ = update::UPDATE_STATE_INSTALLING; + this->update_info_.has_progress = false; + this->publish_state(); + + esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error("Failed to begin OTA"); + this->publish_state(); + return; + } + + uint8_t chunk[CHUNK_SIZE]; + const uint8_t *data_ptr = this->firmware_data_; + size_t remaining = this->firmware_size_; + while (remaining > 0) { + size_t chunk_size = std::min(remaining, static_cast(CHUNK_SIZE)); + memcpy(chunk, data_ptr, chunk_size); + err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); + esp_hosted_slave_ota_end(); // NOLINT + this->state_ = prev_state; + this->status_set_error("Failed to write OTA data"); + this->publish_state(); + return; + } + data_ptr += chunk_size; + remaining -= chunk_size; + App.feed_wdt(); + } + + err = esp_hosted_slave_ota_end(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error("Failed to end OTA"); + this->publish_state(); + return; + } + + // activate new firmware + err = esp_hosted_slave_ota_activate(); // NOLINT + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err)); + this->state_ = prev_state; + this->status_set_error("Failed to activate OTA"); + this->publish_state(); + return; + } + + // update state + ESP_LOGI(TAG, "OTA update successful"); + this->state_ = update::UPDATE_STATE_NO_UPDATE; + this->status_clear_error(); + this->publish_state(); + + // schedule a restart to ensure everything is in sync + ESP_LOGI(TAG, "Restarting in 1 second"); + this->set_timeout(1000, []() { App.safe_reboot(); }); +} + +} // namespace esphome::esp32_hosted +#endif diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.h b/esphome/components/esp32_hosted/update/esp32_hosted_update.h new file mode 100644 index 00000000000..9c087bf72aa --- /dev/null +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.h @@ -0,0 +1,32 @@ +#pragma once + +#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) + +#include "esphome/core/component.h" +#include "esphome/components/update/update_entity.h" +#include + +namespace esphome::esp32_hosted { + +class Esp32HostedUpdate : public update::UpdateEntity, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void perform(bool force) override; + void check() override {} + + void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; } + void set_firmware_size(size_t size) { this->firmware_size_ = size; } + void set_firmware_sha256(const std::array &sha256) { this->firmware_sha256_ = sha256; } + + protected: + const uint8_t *firmware_data_{nullptr}; + size_t firmware_size_{0}; + std::array firmware_sha256_; +}; + +} // namespace esphome::esp32_hosted + +#endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 2fa9d8f5234..398b1d42519 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -336,7 +336,7 @@ void ESP32ImprovComponent::process_incoming_data_() { this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_connecting(sta, false); + wifi::global_wifi_component->start_connecting(sta); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index 79530aacd44..4b021fc4622 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -40,7 +40,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(ESP8266PWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/esp_ldo/esp_ldo.h b/esphome/components/esp_ldo/esp_ldo.h index bafa32db6b9..9edd303e164 100644 --- a/esphome/components/esp_ldo/esp_ldo.h +++ b/esphome/components/esp_ldo/esp_ldo.h @@ -34,7 +34,7 @@ template class AdjustAction : public Action { TEMPLATABLE_VALUE(float, voltage) - void play(Ts... x) override { this->ldo_->adjust_voltage(this->voltage_.value(x...)); } + void play(const Ts &...x) override { this->ldo_->adjust_voltage(this->voltage_.value(x...)); } protected: EspLdo *ldo_; diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b85d6602726..eb6c61a69be 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -94,7 +94,7 @@ void ESPHomeOTAComponent::dump_config() { "Over-The-Air updates:\n" " Address: %s:%u\n" " Version: %d", - network::get_use_address().c_str(), this->port_, USE_OTA_VERSION); + network::get_use_address(), this->port_, USE_OTA_VERSION); #ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { ESP_LOGCONFIG(TAG, " Password configured"); diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 9d2f17440c3..cc2c02d4c09 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 wifi +from esphome.components import socket, wifi from esphome.components.udp import CONF_ON_RECEIVE import esphome.config_validation as cv from esphome.const import ( @@ -17,6 +17,7 @@ from esphome.core import CORE, 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) @@ -120,6 +121,10 @@ async def to_code(config): if CORE.using_arduino: cg.add_library("WiFi", None) + # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks + # This enables low-latency event processing instead of waiting for select() timeout + socket.require_wake_loop_threadsafe() + cg.add_define("USE_ESPNOW") if wifi_channel := config.get(CONF_CHANNEL): cg.add(var.set_wifi_channel(wifi_channel)) diff --git a/esphome/components/espnow/automation.h b/esphome/components/espnow/automation.h index 5415b088fd0..0b266814005 100644 --- a/esphome/components/espnow/automation.h +++ b/esphome/components/espnow/automation.h @@ -36,7 +36,7 @@ template class SendAction : public Action, public Parente void set_wait_for_sent(bool wait_for_sent) { this->flags_.wait_for_sent = wait_for_sent; } void set_continue_on_error(bool continue_on_error) { this->flags_.continue_on_error = continue_on_error; } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; send_callback_t send_callback = [this, x...](esp_err_t status) { if (status == ESP_OK) { @@ -67,7 +67,7 @@ template class SendAction : public Action, public Parente } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { @@ -90,7 +90,7 @@ template class AddPeerAction : public Action, public Pare TEMPLATABLE_VALUE(peer_address_t, address); public: - void play(Ts... x) override { + void play(const Ts &...x) override { peer_address_t address = this->address_.value(x...); this->parent_->add_peer(address.data()); } @@ -100,7 +100,7 @@ template class DeletePeerAction : public Action, public P TEMPLATABLE_VALUE(peer_address_t, address); public: - void play(Ts... x) override { + void play(const Ts &...x) override { peer_address_t address = this->address_.value(x...); this->parent_->del_peer(address.data()); } @@ -109,7 +109,7 @@ template class DeletePeerAction : public Action, public P template class SetChannelAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, channel) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->parent_->is_wifi_enabled()) { return; } diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index b0d5938dbad..d2f136d1c7a 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -4,6 +4,7 @@ #include "espnow_err.h" +#include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/log.h" @@ -97,6 +98,11 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status) // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); // Push always because we're the only producer and the pool ensures we never exceed queue size + + // 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) + App.wake_loop_threadsafe(); +#endif } void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { @@ -114,6 +120,11 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int // Push the packet to the queue global_esp_now->receive_packet_queue_.push(packet); // Push always because we're the only producer and the pool ensures we never exceed queue size + + // 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) + App.wake_loop_threadsafe(); +#endif } ESPNowComponent::ESPNowComponent() { global_esp_now = this; } diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 2f02d227d71..b4d67635c18 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -383,6 +383,7 @@ async def to_code(config): cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) if CONF_MANUAL_IP in config: + cg.add_define("USE_ETHERNET_MANUAL_IP") cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) # Add compile-time define for PHY types with specific code diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 24b6e8154ba..9a46aa2687c 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -381,7 +381,10 @@ void EthernetComponent::dump_config() { break; } - ESP_LOGCONFIG(TAG, "Ethernet:"); + ESP_LOGCONFIG(TAG, + "Ethernet:\n" + " Connected: %s", + YESNO(this->is_connected())); this->dump_connect_params_(); #ifdef USE_ETHERNET_SPI ESP_LOGCONFIG(TAG, @@ -418,8 +421,6 @@ void EthernetComponent::dump_config() { float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } -bool EthernetComponent::can_proceed() { return this->is_connected(); } - network::IPAddresses EthernetComponent::get_ip_addresses() { network::IPAddresses addresses; esp_netif_ip_info_t ip; @@ -552,11 +553,14 @@ void EthernetComponent::start_connect_() { } esp_netif_ip_info_t info; +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { info.ip = this->manual_ip_->static_ip; info.gw = this->manual_ip_->gateway; info.netmask = this->manual_ip_->subnet; - } else { + } else +#endif + { info.ip.addr = 0; info.gw.addr = 0; info.netmask.addr = 0; @@ -577,6 +581,7 @@ void EthernetComponent::start_connect_() { err = esp_netif_set_ip_info(this->eth_netif_, &info); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); +#ifdef USE_ETHERNET_MANUAL_IP if (this->manual_ip_.has_value()) { LwIPLock lock; if (this->manual_ip_->dns1.is_set()) { @@ -589,7 +594,9 @@ void EthernetComponent::start_connect_() { d = this->manual_ip_->dns2; dns_setserver(1, &d); } - } else { + } else +#endif + { err = esp_netif_dhcpc_start(this->eth_netif_); if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) { ESPHL_ERROR_CHECK(err, "DHCPC start error"); @@ -687,13 +694,15 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); } #endif void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } +#ifdef USE_ETHERNET_MANUAL_IP void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } +#endif // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const std::string &EthernetComponent::get_use_address() const { return this->use_address_; } +const char *EthernetComponent::get_use_address() const { return this->use_address_; } -void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { esp_err_t err; diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index d5dda3e3aef..bffed4dc4a3 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -58,7 +58,6 @@ class EthernetComponent : public Component { void loop() override; void dump_config() override; float get_setup_priority() const override; - bool can_proceed() override; void on_powerdown() override { powerdown(); } bool is_connected(); @@ -83,13 +82,15 @@ class EthernetComponent : public Component { void add_phy_register(PHYRegister register_value); #endif void set_type(EthernetType type); +#ifdef USE_ETHERNET_MANUAL_IP void set_manual_ip(const ManualIP &manual_ip); +#endif void set_fixed_mac(const std::array &mac) { this->fixed_mac_ = mac; } network::IPAddresses get_ip_addresses(); network::IPAddress get_dns_address(uint8_t num); - const std::string &get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); void get_eth_mac_address_raw(uint8_t *mac); std::string get_eth_mac_address_pretty(); eth_duplex_t get_duplex_mode(); @@ -114,7 +115,6 @@ class EthernetComponent : public Component { /// @brief Set arbitratry PHY registers from config. void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); - std::string use_address_; #ifdef USE_ETHERNET_SPI uint8_t clk_pin_; uint8_t miso_pin_; @@ -139,7 +139,9 @@ class EthernetComponent : public Component { uint8_t mdc_pin_{23}; uint8_t mdio_pin_{18}; #endif +#ifdef USE_ETHERNET_MANUAL_IP optional manual_ip_{}; +#endif uint32_t connect_begin_; // Group all uint8_t types together (enums and bools) @@ -158,6 +160,11 @@ class EthernetComponent : public Component { esp_eth_handle_t eth_handle_; esp_eth_phy_t *phy_{nullptr}; optional> fixed_mac_; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/event/automation.h b/esphome/components/event/automation.h index 9ebcb654a04..5bdba186871 100644 --- a/esphome/components/event/automation.h +++ b/esphome/components/event/automation.h @@ -11,7 +11,7 @@ template class TriggerEventAction : public Action, public public: TEMPLATABLE_VALUE(std::string, event_type) - void play(Ts... x) override { this->parent_->trigger(this->event_type_.value(x...)); } + void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); } }; class EventTrigger : public Trigger { diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 20549ad0a50..4c74a113885 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -1,5 +1,6 @@ #include "event.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -8,11 +9,11 @@ namespace event { static const char *const TAG = "event"; void Event::trigger(const std::string &event_type) { - // Linear search - faster than std::set for small datasets (1-5 items typical) - const std::string *found = nullptr; - for (const auto &type : this->types_) { - if (type == event_type) { - found = &type; + // Linear search with strcmp - faster than std::set for small datasets (1-5 items typical) + const char *found = nullptr; + for (const char *type : this->types_) { + if (strcmp(type, event_type.c_str()) == 0) { + found = type; break; } } @@ -20,9 +21,28 @@ void Event::trigger(const std::string &event_type) { ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); return; } - last_event_type = found; - ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); + this->last_event_type_ = found; + ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_); this->event_callback_.call(event_type); +#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_event(this); +#endif +} + +void Event::set_event_types(const FixedVector &event_types) { + this->types_.init(event_types.size()); + for (const char *type : event_types) { + this->types_.push_back(type); + } + this->last_event_type_ = nullptr; // Reset when types change +} + +void Event::set_event_types(const std::vector &event_types) { + this->types_.init(event_types.size()); + for (const char *type : event_types) { + this->types_.push_back(type); + } + this->last_event_type_ = nullptr; // Reset when types change } void Event::add_on_event_callback(std::function &&callback) { diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 2f6267a2006..e4b2e0b845b 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include "esphome/core/component.h" #include "esphome/core/entity_base.h" @@ -22,16 +24,39 @@ namespace event { class Event : public EntityBase, public EntityBase_DeviceClass { public: - const std::string *last_event_type; - void trigger(const std::string &event_type); - void set_event_types(const std::initializer_list &event_types) { this->types_ = event_types; } - const FixedVector &get_event_types() const { return this->types_; } + + /// Set the event types supported by this event (from initializer list). + void set_event_types(std::initializer_list event_types) { + this->types_ = event_types; + this->last_event_type_ = nullptr; // Reset when types change + } + /// Set the event types supported by this event (from FixedVector). + void set_event_types(const FixedVector &event_types); + /// Set the event types supported by this event (from vector). + void set_event_types(const std::vector &event_types); + + // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages + void set_event_types(std::initializer_list event_types) = delete; + void set_event_types(const FixedVector &event_types) = delete; + void set_event_types(const std::vector &event_types) = delete; + + /// Return the event types supported by this event. + const FixedVector &get_event_types() const { return this->types_; } + + /// Return the last triggered event type (pointer to string in types_), or nullptr if no event triggered yet. + const char *get_last_event_type() const { return this->last_event_type_; } + void add_on_event_callback(std::function &&callback); protected: CallbackManager event_callback_; - FixedVector types_; + FixedVector types_; + + private: + /// Last triggered event type - must point to entry in types_ to ensure valid lifetime. + /// Set by trigger() after validation, reset to nullptr when types_ changes. + const char *last_event_type_{nullptr}; }; } // namespace event diff --git a/esphome/components/ezo/automation.h b/esphome/components/ezo/automation.h index 19427b91591..a4a6fa30146 100644 --- a/esphome/components/ezo/automation.h +++ b/esphome/components/ezo/automation.h @@ -17,35 +17,35 @@ class LedTrigger : public Trigger { class CustomTrigger : public Trigger { public: explicit CustomTrigger(EZOSensor *ezo) { - ezo->add_custom_callback([this](std::string value) { this->trigger(std::move(value)); }); + 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](std::string value) { this->trigger(std::move(value)); }); + 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](std::string value) { this->trigger(std::move(value)); }); + 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](std::string value) { this->trigger(std::move(value)); }); + 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](std::string value) { this->trigger(std::move(value)); }); + ezo->add_device_infomation_callback([this](const std::string &value) { this->trigger(value); }); } }; diff --git a/esphome/components/ezo_pmp/ezo_pmp.h b/esphome/components/ezo_pmp/ezo_pmp.h index 671e1248103..d4917e7f4b4 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.h +++ b/esphome/components/ezo_pmp/ezo_pmp.h @@ -119,7 +119,7 @@ template class EzoPMPFindAction : public Action { public: EzoPMPFindAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->find(); } + void play(const Ts &...x) override { this->ezopmp_->find(); } protected: EzoPMP *ezopmp_; @@ -129,7 +129,7 @@ template class EzoPMPDoseContinuouslyAction : public Actionezopmp_->dose_continuously(); } + void play(const Ts &...x) override { this->ezopmp_->dose_continuously(); } protected: EzoPMP *ezopmp_; @@ -139,7 +139,7 @@ template class EzoPMPDoseVolumeAction : public Action { public: EzoPMPDoseVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->dose_volume(this->volume_.value(x...)); } TEMPLATABLE_VALUE(double, volume) protected: @@ -150,7 +150,7 @@ template class EzoPMPDoseVolumeOverTimeAction : public Actionezopmp_->dose_volume_over_time(this->volume_.value(x...), this->duration_.value(x...)); } TEMPLATABLE_VALUE(double, volume) @@ -164,7 +164,7 @@ template class EzoPMPDoseWithConstantFlowRateAction : public Act public: EzoPMPDoseWithConstantFlowRateAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { + void play(const Ts &...x) override { this->ezopmp_->dose_with_constant_flow_rate(this->volume_.value(x...), this->duration_.value(x...)); } TEMPLATABLE_VALUE(double, volume) @@ -178,7 +178,7 @@ template class EzoPMPSetCalibrationVolumeAction : public Action< public: EzoPMPSetCalibrationVolumeAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->set_calibration_volume(this->volume_.value(x...)); } TEMPLATABLE_VALUE(double, volume) protected: @@ -189,7 +189,7 @@ template class EzoPMPClearTotalVolumeDispensedAction : public Ac public: EzoPMPClearTotalVolumeDispensedAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->clear_total_volume_dosed(); } + void play(const Ts &...x) override { this->ezopmp_->clear_total_volume_dosed(); } protected: EzoPMP *ezopmp_; @@ -199,7 +199,7 @@ template class EzoPMPClearCalibrationAction : public Actionezopmp_->clear_calibration(); } + void play(const Ts &...x) override { this->ezopmp_->clear_calibration(); } protected: EzoPMP *ezopmp_; @@ -209,7 +209,7 @@ template class EzoPMPPauseDosingAction : public Action { public: EzoPMPPauseDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->pause_dosing(); } + void play(const Ts &...x) override { this->ezopmp_->pause_dosing(); } protected: EzoPMP *ezopmp_; @@ -219,7 +219,7 @@ template class EzoPMPStopDosingAction : public Action { public: EzoPMPStopDosingAction(EzoPMP *ezopmp) : ezopmp_(ezopmp) {} - void play(Ts... x) override { this->ezopmp_->stop_dosing(); } + void play(const Ts &...x) override { this->ezopmp_->stop_dosing(); } protected: EzoPMP *ezopmp_; @@ -229,7 +229,7 @@ template class EzoPMPChangeI2CAddressAction : public Actionezopmp_->change_i2c_address(this->address_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->change_i2c_address(this->address_.value(x...)); } TEMPLATABLE_VALUE(int, address) protected: @@ -240,7 +240,7 @@ template class EzoPMPArbitraryCommandAction : public Actionezopmp_->exec_arbitrary_command(this->command_.value(x...)); } + void play(const Ts &...x) override { this->ezopmp_->exec_arbitrary_command(this->command_.value(x...)); } TEMPLATABLE_VALUE(std::string, command) protected: diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 48de8d66fbd..ce1db6fc645 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -15,7 +15,7 @@ template class TurnOnAction : public Action { TEMPLATABLE_VALUE(int, speed) TEMPLATABLE_VALUE(FanDirection, direction) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->state_->turn_on(); if (this->oscillating_.has_value()) { call.set_oscillating(this->oscillating_.value(x...)); @@ -36,7 +36,7 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(Fan *state) : state_(state) {} - void play(Ts... x) override { this->state_->turn_off().perform(); } + void play(const Ts &...x) override { this->state_->turn_off().perform(); } Fan *state_; }; @@ -45,7 +45,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Fan *state) : state_(state) {} - void play(Ts... x) override { this->state_->toggle().perform(); } + void play(const Ts &...x) override { this->state_->toggle().perform(); } Fan *state_; }; @@ -56,7 +56,7 @@ template class CycleSpeedAction : public Action { TEMPLATABLE_VALUE(bool, no_off_cycle) - void play(Ts... x) override { + void play(const Ts &...x) override { // check to see if fan supports speeds and is on if (this->state_->get_traits().supported_speed_count()) { if (this->state_->state) { @@ -97,7 +97,7 @@ template class CycleSpeedAction : public Action { template class FanIsOnCondition : public Condition { public: explicit FanIsOnCondition(Fan *state) : state_(state) {} - bool check(Ts... x) override { return this->state_->state; } + bool check(const Ts &...x) override { return this->state_->state; } protected: Fan *state_; @@ -105,7 +105,7 @@ template class FanIsOnCondition : public Condition { template class FanIsOffCondition : public Condition { public: explicit FanIsOffCondition(Fan *state) : state_(state) {} - bool check(Ts... x) override { return !this->state_->state; } + bool check(const Ts &...x) override { return !this->state_->state; } protected: Fan *state_; @@ -215,8 +215,9 @@ class FanPresetSetTrigger : public Trigger { const auto *preset_mode = state->get_preset_mode(); auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; - if (should_trigger && preset_mode != nullptr) { - this->trigger(preset_mode); + if (should_trigger) { + // Trigger with empty string when nullptr to maintain backward compatibility + this->trigger(preset_mode != nullptr ? preset_mode : ""); } }); this->last_preset_mode_ = state->get_preset_mode(); diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index cfc09f4d533..d37825a6513 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,4 +1,6 @@ #include "fan.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -121,10 +123,13 @@ void FanRestoreState::apply(Fan &fan) { fan.speed = this->speed; fan.direction = this->direction; - // Use stored preset index to get preset name from traits - const auto &preset_modes = fan.get_traits().supported_preset_modes(); - if (this->preset_mode < preset_modes.size()) { - fan.set_preset_mode_(preset_modes[this->preset_mode]); + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { + // Use stored preset index to get preset name from traits + const auto &preset_modes = traits.supported_preset_modes(); + if (this->preset_mode < preset_modes.size()) { + fan.set_preset_mode_(preset_modes[this->preset_mode]); + } } fan.publish_state(); @@ -174,10 +179,13 @@ void Fan::publish_state() { ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); } const char *preset = this->get_preset_mode(); - if (traits.supports_preset_modes() && preset != nullptr) { + if (preset != nullptr) { ESP_LOGD(TAG, " Preset Mode: %s", preset); } this->state_callback_.call(); +#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_fan_update(this); +#endif this->save_state_(); } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 33e546b2bb7..e38a80dbbe3 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -156,6 +156,8 @@ class Fan : public EntityBase { CallbackManager state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; + + private: const char *preset_mode_{nullptr}; }; diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h index 590c709c224..370b26f56a3 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.h +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -273,7 +273,7 @@ template class EnrollmentAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, finger_id) TEMPLATABLE_VALUE(uint8_t, num_scans) - void play(Ts... x) override { + void play(const Ts &...x) override { auto finger_id = this->finger_id_.value(x...); auto num_scans = this->num_scans_.value(x...); if (num_scans) { @@ -287,14 +287,14 @@ template class EnrollmentAction : public Action, public P template class CancelEnrollmentAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->finish_enrollment(1); } + void play(const Ts &...x) override { this->parent_->finish_enrollment(1); } }; template class DeleteAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, finger_id) - void play(Ts... x) override { + void play(const Ts &...x) override { auto finger_id = this->finger_id_.value(x...); this->parent_->delete_fingerprint(finger_id); } @@ -302,14 +302,14 @@ template class DeleteAction : public Action, public Paren template class DeleteAllAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->delete_all_fingerprints(); } + void play(const Ts &...x) override { this->parent_->delete_all_fingerprints(); } }; template class LEDControlAction : public Action, public Parented { public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { auto state = this->state_.value(x...); this->parent_->led_control(state); } @@ -322,7 +322,7 @@ template class AuraLEDControlAction : public Action, publ TEMPLATABLE_VALUE(uint8_t, color) TEMPLATABLE_VALUE(uint8_t, count) - void play(Ts... x) override { + void play(const Ts &...x) override { auto state = this->state_.value(x...); auto speed = this->speed_.value(x...); auto color = this->color_.value(x...); diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 4c156ab24b8..6c218f03d9c 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -62,7 +62,6 @@ void GDK101Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } #ifdef USE_SENSOR - LOG_SENSOR(" ", "Firmware Version", this->fw_version_sensor_); LOG_SENSOR(" ", "Average Radaition Dose per 1 minute", this->rad_1m_sensor_); LOG_SENSOR(" ", "Average Radaition Dose per 10 minutes", this->rad_10m_sensor_); LOG_SENSOR(" ", "Status", this->status_sensor_); @@ -72,6 +71,10 @@ void GDK101Component::dump_config() { #ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "Vibration Status", this->vibration_binary_sensor_); #endif // USE_BINARY_SENSOR + +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "Firmware Version", this->fw_version_text_sensor_); +#endif // USE_TEXT_SENSOR } float GDK101Component::get_setup_priority() const { return setup_priority::DATA; } @@ -153,18 +156,18 @@ bool GDK101Component::read_status_(uint8_t *data) { } bool GDK101Component::read_fw_version_(uint8_t *data) { -#ifdef USE_SENSOR - if (this->fw_version_sensor_ != nullptr) { +#ifdef USE_TEXT_SENSOR + if (this->fw_version_text_sensor_ != nullptr) { if (!this->read_bytes(GDK101_REG_READ_FIRMWARE, data, 2)) { ESP_LOGE(TAG, "Updating GDK101 failed!"); return false; } - const float fw_version = data[0] + (data[1] / 10.0f); + const std::string fw_version_str = str_sprintf("%d.%d", data[0], data[1]); - this->fw_version_sensor_->publish_state(fw_version); + this->fw_version_text_sensor_->publish_state(fw_version_str); } -#endif // USE_SENSOR +#endif // USE_TEXT_SENSOR return true; } diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h index 460e72ac89e..f250a42a540 100644 --- a/esphome/components/gdk101/gdk101.h +++ b/esphome/components/gdk101/gdk101.h @@ -8,6 +8,9 @@ #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" #endif // USE_BINARY_SENSOR +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif // USE_TEXT_SENSOR #include "esphome/components/i2c/i2c.h" namespace esphome { @@ -25,12 +28,14 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice { SUB_SENSOR(rad_1m) SUB_SENSOR(rad_10m) SUB_SENSOR(status) - SUB_SENSOR(fw_version) SUB_SENSOR(measurement_duration) #endif // USE_SENSOR #ifdef USE_BINARY_SENSOR SUB_BINARY_SENSOR(vibration) #endif // USE_BINARY_SENSOR +#ifdef USE_TEXT_SENSOR + SUB_TEXT_SENSOR(fw_version) +#endif // USE_TEXT_SENSOR public: void setup() override; diff --git a/esphome/components/gdk101/sensor.py b/esphome/components/gdk101/sensor.py index d04e0b8367b..6cf89e0fd4c 100644 --- a/esphome/components/gdk101/sensor.py +++ b/esphome/components/gdk101/sensor.py @@ -40,9 +40,8 @@ CONFIG_SCHEMA = cv.Schema( device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_VERSION): sensor.sensor_schema( - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - accuracy_decimals=1, + cv.Optional(CONF_VERSION): cv.invalid( + "The 'version' option has been moved to the `text_sensor` component." ), cv.Optional(CONF_STATUS): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, @@ -71,10 +70,6 @@ async def to_code(config): sens = await sensor.new_sensor(radiation_dose_per_10m) cg.add(hub.set_rad_10m_sensor(sens)) - if version_config := config.get(CONF_VERSION): - sens = await sensor.new_sensor(version_config) - cg.add(hub.set_fw_version_sensor(sens)) - if status_config := config.get(CONF_STATUS): sens = await sensor.new_sensor(status_config) cg.add(hub.set_status_sensor(sens)) diff --git a/esphome/components/gdk101/text_sensor.py b/esphome/components/gdk101/text_sensor.py new file mode 100644 index 00000000000..703e68493af --- /dev/null +++ b/esphome/components/gdk101/text_sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP + +from . import CONF_GDK101_ID, GDK101Component + +DEPENDENCIES = ["gdk101"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_GDK101_ID): cv.use_id(GDK101Component), + cv.Required(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_GDK101_ID]) + var = await text_sensor.new_text_sensor(config[CONF_VERSION]) + cg.add(hub.set_fw_version_text_sensor(var)) diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 4c6a12aa720..1d2a08937ee 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -134,7 +134,7 @@ template class GlobalVarSetAction : public Actionparent_->value() = this->value_.value(x...); } + void play(const Ts &...x) override { this->parent_->value() = this->value_.value(x...); } protected: C *parent_; diff --git a/esphome/components/gp8403/__init__.py b/esphome/components/gp8403/__init__.py index 96f1807688d..83859a40302 100644 --- a/esphome/components/gp8403/__init__.py +++ b/esphome/components/gp8403/__init__.py @@ -1,19 +1,25 @@ import esphome.codegen as cg from esphome.components import i2c import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_VOLTAGE +from esphome.const import CONF_ID, CONF_MODEL, CONF_VOLTAGE -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@sebydocky"] DEPENDENCIES = ["i2c"] MULTI_CONF = True gp8403_ns = cg.esphome_ns.namespace("gp8403") -GP8403 = gp8403_ns.class_("GP8403", cg.Component, i2c.I2CDevice) +GP8403Component = gp8403_ns.class_("GP8403Component", cg.Component, i2c.I2CDevice) GP8403Voltage = gp8403_ns.enum("GP8403Voltage") +GP8403Model = gp8403_ns.enum("GP8403Model") CONF_GP8403_ID = "gp8403_id" +MODELS = { + "GP8403": GP8403Model.GP8403, + "GP8413": GP8403Model.GP8413, +} + VOLTAGES = { "5V": GP8403Voltage.GP8403_VOLTAGE_5V, "10V": GP8403Voltage.GP8403_VOLTAGE_10V, @@ -22,7 +28,8 @@ VOLTAGES = { CONFIG_SCHEMA = ( cv.Schema( { - cv.GenerateID(): cv.declare_id(GP8403), + cv.GenerateID(): cv.declare_id(GP8403Component), + cv.Optional(CONF_MODEL, default="GP8403"): cv.enum(MODELS, upper=True), cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True), } ) @@ -35,5 +42,5 @@ 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) - + cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_voltage(config[CONF_VOLTAGE])) diff --git a/esphome/components/gp8403/gp8403.cpp b/esphome/components/gp8403/gp8403.cpp index 5107e96dee5..11c2f9a7c06 100644 --- a/esphome/components/gp8403/gp8403.cpp +++ b/esphome/components/gp8403/gp8403.cpp @@ -8,16 +8,48 @@ namespace gp8403 { static const char *const TAG = "gp8403"; static const uint8_t RANGE_REGISTER = 0x01; +static const uint8_t OUTPUT_REGISTER = 0x02; -void GP8403::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } +const LogString *model_to_string(GP8403Model model) { + switch (model) { + case GP8403Model::GP8403: + return LOG_STR("GP8403"); + case GP8403Model::GP8413: + return LOG_STR("GP8413"); + } + return LOG_STR("Unknown"); +}; -void GP8403::dump_config() { +void GP8403Component::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } + +void GP8403Component::dump_config() { ESP_LOGCONFIG(TAG, "GP8403:\n" - " Voltage: %dV", - this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10); + " Voltage: %dV\n" + " Model: %s", + this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10, LOG_STR_ARG(model_to_string(this->model_))); LOG_I2C_DEVICE(this); } +void GP8403Component::write_state(float state, uint8_t channel) { + uint16_t val = 0; + switch (this->model_) { + case GP8403Model::GP8403: + val = ((uint16_t) (4095 * state)) << 4; + break; + case GP8403Model::GP8413: + val = ((uint16_t) (32767 * state)) << 1; + break; + default: + ESP_LOGE(TAG, "Unknown model %s", LOG_STR_ARG(model_to_string(this->model_))); + return; + } + ESP_LOGV(TAG, "Calculated DAC value: %" PRIu16, val); + i2c::ErrorCode err = this->write_register(OUTPUT_REGISTER + (2 * channel), (uint8_t *) &val, 2); + if (err != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Error writing to %s, code %d", LOG_STR_ARG(model_to_string(this->model_)), err); + } +} + } // namespace gp8403 } // namespace esphome diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 9f493d39e38..6613187b206 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -11,15 +11,24 @@ enum GP8403Voltage { GP8403_VOLTAGE_10V = 0x11, }; -class GP8403 : public Component, public i2c::I2CDevice { +enum GP8403Model { + GP8403, + GP8413, +}; + +class GP8403Component : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - + float get_setup_priority() const override { return setup_priority::DATA; } + void set_model(GP8403Model model) { this->model_ = model; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } + void write_state(float state, uint8_t channel); + protected: GP8403Voltage voltage_; + GP8403Model model_{GP8403Model::GP8403}; }; } // namespace gp8403 diff --git a/esphome/components/gp8403/output/__init__.py b/esphome/components/gp8403/output/__init__.py index dc57833f4a7..5245c405db6 100644 --- a/esphome/components/gp8403/output/__init__.py +++ b/esphome/components/gp8403/output/__init__.py @@ -3,7 +3,7 @@ from esphome.components import i2c, output import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ID -from .. import CONF_GP8403_ID, GP8403, gp8403_ns +from .. import CONF_GP8403_ID, GP8403Component, gp8403_ns DEPENDENCIES = ["gp8403"] @@ -14,7 +14,7 @@ GP8403Output = gp8403_ns.class_( CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(GP8403Output), - cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403), + cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403Component), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/gp8403/output/gp8403_output.cpp b/esphome/components/gp8403/output/gp8403_output.cpp index edb6972184d..dfdc2d6ccb1 100644 --- a/esphome/components/gp8403/output/gp8403_output.cpp +++ b/esphome/components/gp8403/output/gp8403_output.cpp @@ -7,8 +7,6 @@ namespace gp8403 { static const char *const TAG = "gp8403.output"; -static const uint8_t OUTPUT_REGISTER = 0x02; - void GP8403Output::dump_config() { ESP_LOGCONFIG(TAG, "GP8403 Output:\n" @@ -16,13 +14,7 @@ void GP8403Output::dump_config() { this->channel_); } -void GP8403Output::write_state(float state) { - uint16_t value = ((uint16_t) (state * 4095)) << 4; - i2c::ErrorCode err = this->parent_->write_register(OUTPUT_REGISTER + (2 * this->channel_), (uint8_t *) &value, 2); - if (err != i2c::ERROR_OK) { - ESP_LOGE(TAG, "Error writing to GP8403, code %d", err); - } -} +void GP8403Output::write_state(float state) { this->parent_->write_state(state, this->channel_); } } // namespace gp8403 } // namespace esphome diff --git a/esphome/components/gp8403/output/gp8403_output.h b/esphome/components/gp8403/output/gp8403_output.h index 71e5efb1cb5..c0d6650500e 100644 --- a/esphome/components/gp8403/output/gp8403_output.h +++ b/esphome/components/gp8403/output/gp8403_output.h @@ -8,13 +8,11 @@ namespace esphome { namespace gp8403 { -class GP8403Output : public Component, public output::FloatOutput, public Parented { +class GP8403Output : public Component, public output::FloatOutput, public Parented { public: void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA - 1; } - void set_channel(uint8_t channel) { this->channel_ = channel; } - void write_state(float state) override; protected: diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index ac6ace96ee3..88bb306408f 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -235,7 +235,7 @@ void GraphLegend::init(Graph *g) { std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (this->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh); if (fw > valw) @@ -371,7 +371,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (legend_->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str()); ESP_LOGV(TAG, " value: %s", valstr.c_str()); diff --git a/esphome/components/grove_tb6612fng/grove_tb6612fng.h b/esphome/components/grove_tb6612fng/grove_tb6612fng.h index 68281117e76..a36cb85cffc 100644 --- a/esphome/components/grove_tb6612fng/grove_tb6612fng.h +++ b/esphome/components/grove_tb6612fng/grove_tb6612fng.h @@ -168,7 +168,7 @@ class GROVETB6612FNGMotorRunAction : public Action, public Parentedchannel_.value(x...); auto speed = this->speed_.value(x...); this->parent_->dc_motor_run(channel, speed); @@ -180,7 +180,7 @@ class GROVETB6612FNGMotorBrakeAction : public Action, public Parentedparent_->dc_motor_brake(this->channel_.value(x...)); } + void play(const Ts &...x) override { this->parent_->dc_motor_brake(this->channel_.value(x...)); } }; template @@ -188,19 +188,19 @@ class GROVETB6612FNGMotorStopAction : public Action, public Parentedparent_->dc_motor_stop(this->channel_.value(x...)); } + void play(const Ts &...x) override { this->parent_->dc_motor_stop(this->channel_.value(x...)); } }; template class GROVETB6612FNGMotorStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->standby(); } + void play(const Ts &...x) override { this->parent_->standby(); } }; template class GROVETB6612FNGMotorNoStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->not_standby(); } + void play(const Ts &...x) override { this->parent_->not_standby(); } }; template @@ -208,7 +208,7 @@ class GROVETB6612FNGMotorChangeAddressAction : public Action, public Pare public: TEMPLATABLE_VALUE(uint8_t, address) - void play(Ts... x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_i2c_addr(this->address_.value(x...)); } }; } // namespace grove_tb6612fng diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 4810867d4ba..992a86cc21a 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -34,8 +34,8 @@ void GT911Touchscreen::setup() { this->interrupt_pin_->digital_write(false); } delay(2); - this->reset_pin_->digital_write(true); // wait 50ms after reset - this->set_timeout(50, [this] { this->setup_internal_(); }); + this->reset_pin_->digital_write(true); // wait at least T3+T4 ms as per the datasheet + this->set_timeout(5 + 50 + 1, [this] { this->setup_internal_(); }); return; } this->setup_internal_(); diff --git a/esphome/components/haier/automation.h b/esphome/components/haier/automation.h index 55df7ecc1d6..c1ce7c01ea4 100644 --- a/esphome/components/haier/automation.h +++ b/esphome/components/haier/automation.h @@ -10,7 +10,7 @@ namespace haier { template class DisplayOnAction : public Action { public: DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_display_state(true); } + void play(const Ts &...x) { this->parent_->set_display_state(true); } protected: HaierClimateBase *parent_; @@ -19,7 +19,7 @@ template class DisplayOnAction : public Action { template class DisplayOffAction : public Action { public: DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_display_state(false); } + void play(const Ts &...x) { this->parent_->set_display_state(false); } protected: HaierClimateBase *parent_; @@ -28,7 +28,7 @@ template class DisplayOffAction : public Action { template class BeeperOnAction : public Action { public: BeeperOnAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_beeper_state(true); } + void play(const Ts &...x) { this->parent_->set_beeper_state(true); } protected: HonClimate *parent_; @@ -37,7 +37,7 @@ template class BeeperOnAction : public Action { template class BeeperOffAction : public Action { public: BeeperOffAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_beeper_state(false); } + void play(const Ts &...x) { this->parent_->set_beeper_state(false); } protected: HonClimate *parent_; @@ -47,7 +47,7 @@ template class VerticalAirflowAction : public Action { public: VerticalAirflowAction(HonClimate *parent) : parent_(parent) {} TEMPLATABLE_VALUE(hon_protocol::VerticalSwingMode, direction) - void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } + void play(const Ts &...x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); } protected: HonClimate *parent_; @@ -57,7 +57,7 @@ template class HorizontalAirflowAction : public Action { public: HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {} TEMPLATABLE_VALUE(hon_protocol::HorizontalSwingMode, direction) - void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } + void play(const Ts &...x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); } protected: HonClimate *parent_; @@ -66,7 +66,7 @@ template class HorizontalAirflowAction : public Action { template class HealthOnAction : public Action { public: HealthOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_health_mode(true); } + void play(const Ts &...x) { this->parent_->set_health_mode(true); } protected: HaierClimateBase *parent_; @@ -75,7 +75,7 @@ template class HealthOnAction : public Action { template class HealthOffAction : public Action { public: HealthOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->set_health_mode(false); } + void play(const Ts &...x) { this->parent_->set_health_mode(false); } protected: HaierClimateBase *parent_; @@ -84,7 +84,7 @@ template class HealthOffAction : public Action { template class StartSelfCleaningAction : public Action { public: StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_self_cleaning(); } + void play(const Ts &...x) { this->parent_->start_self_cleaning(); } protected: HonClimate *parent_; @@ -93,7 +93,7 @@ template class StartSelfCleaningAction : public Action { template class StartSteriCleaningAction : public Action { public: StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_steri_cleaning(); } + void play(const Ts &...x) { this->parent_->start_steri_cleaning(); } protected: HonClimate *parent_; @@ -102,7 +102,7 @@ template class StartSteriCleaningAction : public Action { template class PowerOnAction : public Action { public: PowerOnAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->send_power_on_command(); } + void play(const Ts &...x) { this->parent_->send_power_on_command(); } protected: HaierClimateBase *parent_; @@ -111,7 +111,7 @@ template class PowerOnAction : public Action { template class PowerOffAction : public Action { public: PowerOffAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->send_power_off_command(); } + void play(const Ts &...x) { this->parent_->send_power_off_command(); } protected: HaierClimateBase *parent_; @@ -120,7 +120,7 @@ template class PowerOffAction : public Action { template class PowerToggleAction : public Action { public: PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->toggle_power(); } + void play(const Ts &...x) { this->parent_->toggle_power(); } protected: HaierClimateBase *parent_; diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 143c7c18530..ec1e8ada0e1 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -49,7 +49,7 @@ template class BrakeAction : public Action { public: explicit BrakeAction(HBridgeFan *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->brake(); } + void play(const Ts &...x) override { this->parent_->brake(); } HBridgeFan *parent_; }; diff --git a/esphome/components/hlk_fm22x/__init__.py b/esphome/components/hlk_fm22x/__init__.py new file mode 100644 index 00000000000..efd64b65139 --- /dev/null +++ b/esphome/components/hlk_fm22x/__init__.py @@ -0,0 +1,247 @@ +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_DIRECTION, + CONF_ID, + CONF_NAME, + CONF_ON_ENROLLMENT_DONE, + CONF_ON_ENROLLMENT_FAILED, + CONF_TRIGGER_ID, +) + +CODEOWNERS = ["@OnFreund"] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["binary_sensor", "sensor", "text_sensor"] +MULTI_CONF = True + +CONF_HLK_FM22X_ID = "hlk_fm22x_id" +CONF_FACE_ID = "face_id" +CONF_ON_FACE_SCAN_MATCHED = "on_face_scan_matched" +CONF_ON_FACE_SCAN_UNMATCHED = "on_face_scan_unmatched" +CONF_ON_FACE_SCAN_INVALID = "on_face_scan_invalid" +CONF_ON_FACE_INFO = "on_face_info" + +hlk_fm22x_ns = cg.esphome_ns.namespace("hlk_fm22x") +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) +ScanAction = hlk_fm22x_ns.class_("ScanAction", automation.Action) +ResetAction = hlk_fm22x_ns.class_("ResetAction", automation.Action) + +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_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 + ), + } + ), + } + ) + .extend(cv.polling_component_schema("50ms")) + .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_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 + ) + + 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) + + 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) + + for conf in config.get(CONF_ON_FACE_INFO, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, + [ + (cg.int16, "status"), + (cg.int16, "left"), + (cg.int16, "top"), + (cg.int16, "right"), + (cg.int16, "bottom"), + (cg.int16, "yaw"), + (cg.int16, "pitch"), + (cg.int16, "roll"), + ], + 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.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) + + +@automation.register_action( + "hlk_fm22x.enroll", + EnrollmentAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + cv.Required(CONF_NAME): cv.templatable(cv.string), + cv.Required(CONF_DIRECTION): cv.templatable(cv.uint8_t), + }, + key=CONF_NAME, + ), +) +async def hlk_fm22x_enroll_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_NAME], args, cg.std_string) + cg.add(var.set_name(template_)) + template_ = await cg.templatable(config[CONF_DIRECTION], args, cg.uint8) + cg.add(var.set_direction(template_)) + return var + + +@automation.register_action( + "hlk_fm22x.delete", + DeleteAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + cv.Required(CONF_FACE_ID): cv.templatable(cv.uint16_t), + }, + key=CONF_FACE_ID, + ), +) +async def hlk_fm22x_delete_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_FACE_ID], args, cg.int16) + cg.add(var.set_face_id(template_)) + return var + + +@automation.register_action( + "hlk_fm22x.delete_all", + DeleteAllAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + } + ), +) +async def hlk_fm22x_delete_all_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "hlk_fm22x.scan", + ScanAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + } + ), +) +async def hlk_fm22x_scan_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "hlk_fm22x.reset", + ResetAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(HlkFm22xComponent), + } + ), +) +async def hlk_fm22x_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/hlk_fm22x/binary_sensor.py b/esphome/components/hlk_fm22x/binary_sensor.py new file mode 100644 index 00000000000..3620f33ac0d --- /dev/null +++ b/esphome/components/hlk_fm22x/binary_sensor.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ICON, ICON_KEY_PLUS + +from . import CONF_HLK_FM22X_ID, HlkFm22xComponent + +DEPENDENCIES = ["hlk_fm22x"] + +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( + { + cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent), + cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon, + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_HLK_FM22X_ID]) + var = await binary_sensor.new_binary_sensor(config) + cg.add(hub.set_enrolling_binary_sensor(var)) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp new file mode 100644 index 00000000000..ab15a2340d3 --- /dev/null +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -0,0 +1,325 @@ +#include "hlk_fm22x.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include +#include + +namespace esphome::hlk_fm22x { + +static const char *const TAG = "hlk_fm22x"; + +void HlkFm22xComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); + this->set_enrolling_(false); + while (this->available()) { + this->read(); + } + this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); +} + +void HlkFm22xComponent::update() { + if (this->active_command_ != HlkFm22xCommand::NONE) { + if (this->wait_cycles_ > 600) { + ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_); + if (HlkFm22xCommand::RESET == this->active_command_) { + this->mark_failed(); + } else { + this->reset(); + } + } + } + this->recv_command_(); +} + +void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) { + if (name.length() > 31) { + ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str()); + return; + } + ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str()); + std::array data{}; + data[0] = 0; // admin + std::copy(name.begin(), name.end(), data.begin() + 1); + // Remaining bytes are already zero-initialized + data[33] = (uint8_t) direction; + data[34] = 10; // timeout + this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size()); + this->set_enrolling_(true); +} + +void HlkFm22xComponent::scan_face() { + ESP_LOGI(TAG, "Verify face"); + static const uint8_t DATA[] = {0, 0}; + this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA)); +} + +void HlkFm22xComponent::delete_face(int16_t face_id) { + ESP_LOGI(TAG, "Deleting face in slot %d", face_id); + const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)}; + this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data)); +} + +void HlkFm22xComponent::delete_all_faces() { + ESP_LOGI(TAG, "Deleting all stored faces"); + this->send_command_(HlkFm22xCommand::DELETE_ALL_FACES); +} + +void HlkFm22xComponent::get_face_count_() { + ESP_LOGD(TAG, "Getting face count"); + this->send_command_(HlkFm22xCommand::GET_ALL_FACE_IDS); +} + +void HlkFm22xComponent::reset() { + ESP_LOGI(TAG, "Resetting module"); + this->active_command_ = HlkFm22xCommand::NONE; + this->wait_cycles_ = 0; + this->set_enrolling_(false); + this->send_command_(HlkFm22xCommand::RESET); +} + +void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) { + ESP_LOGV(TAG, "Send command: 0x%.2X", command); + if (this->active_command_ != HlkFm22xCommand::NONE) { + ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_); + return; + } + this->wait_cycles_ = 0; + this->active_command_ = command; + while (this->available()) + this->read(); + this->write((uint8_t) (START_CODE >> 8)); + this->write((uint8_t) (START_CODE & 0xFF)); + this->write((uint8_t) command); + uint16_t data_size = size; + this->write((uint8_t) (data_size >> 8)); + this->write((uint8_t) (data_size & 0xFF)); + + uint8_t checksum = 0; + checksum ^= (uint8_t) command; + checksum ^= (data_size >> 8); + checksum ^= (data_size & 0xFF); + for (size_t i = 0; i < size; i++) { + this->write(data[i]); + checksum ^= data[i]; + } + + this->write(checksum); + this->active_command_ = command; + this->wait_cycles_ = 0; +} + +void HlkFm22xComponent::recv_command_() { + uint8_t byte, checksum = 0; + uint16_t length = 0; + + if (this->available() < 7) { + ++this->wait_cycles_; + return; + } + this->wait_cycles_ = 0; + + if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) { + ESP_LOGE(TAG, "Invalid start code"); + return; + } + + byte = this->read(); + checksum ^= byte; + HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte; + + byte = this->read(); + checksum ^= byte; + length = byte << 8; + byte = this->read(); + checksum ^= byte; + length |= byte; + + std::vector data; + data.reserve(length); + for (uint16_t idx = 0; idx < length; ++idx) { + byte = this->read(); + checksum ^= byte; + data.push_back(byte); + } + + ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty(data).c_str()); + + byte = this->read(); + if (byte != checksum) { + ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte); + return; + } + switch (response_type) { + case HlkFm22xResponseType::NOTE: + this->handle_note_(data); + break; + case HlkFm22xResponseType::REPLY: + this->handle_reply_(data); + break; + default: + ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); + break; + } +} + +void HlkFm22xComponent::handle_note_(const std::vector &data) { + switch (data[0]) { + case HlkFm22xNoteType::FACE_STATE: + if (data.size() < 17) { + ESP_LOGE(TAG, "Invalid face note data size: %u", data.size()); + break; + } + { + int16_t info[8]; + uint8_t offset = 1; + for (int16_t &i : info) { + i = ((int16_t) data[offset + 1] << 8) | data[offset]; + offset += 2; + } + ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d", + info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]); + this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]); + } + break; + case HlkFm22xNoteType::READY: + ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_); + switch (this->active_command_) { + case HlkFm22xCommand::ENROLL: + this->set_enrolling_(false); + this->enrollment_failed_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT); + break; + case HlkFm22xCommand::VERIFY: + this->face_scan_invalid_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT); + break; + default: + break; + } + this->active_command_ = HlkFm22xCommand::NONE; + this->wait_cycles_ = 0; + break; + default: + ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]); + break; + } +} + +void HlkFm22xComponent::handle_reply_(const std::vector &data) { + auto expected = this->active_command_; + this->active_command_ = HlkFm22xCommand::NONE; + if (data[0] != (uint8_t) expected) { + ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]); + return; + } + + if (data[1] != HlkFm22xResult::SUCCESS) { + ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]); + switch (expected) { + case HlkFm22xCommand::ENROLL: + this->set_enrolling_(false); + this->enrollment_failed_callback_.call(data[1]); + break; + case HlkFm22xCommand::VERIFY: + if (data[1] == HlkFm22xResult::REJECTED) { + this->face_scan_unmatched_callback_.call(); + } else { + this->face_scan_invalid_callback_.call(data[1]); + } + break; + default: + break; + } + return; + } + switch (expected) { + case HlkFm22xCommand::VERIFY: { + int16_t face_id = ((int16_t) data[2] << 8) | data[3]; + std::string name(data.begin() + 4, data.begin() + 36); + ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str()); + if (this->last_face_id_sensor_ != nullptr) { + this->last_face_id_sensor_->publish_state(face_id); + } + if (this->last_face_name_text_sensor_ != nullptr) { + this->last_face_name_text_sensor_->publish_state(name); + } + this->face_scan_matched_callback_.call(face_id, name); + break; + } + case HlkFm22xCommand::ENROLL: { + int16_t face_id = ((int16_t) data[2] << 8) | data[3]; + HlkFm22xFaceDirection direction = (HlkFm22xFaceDirection) data[4]; + ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction); + this->enrollment_done_callback_.call(face_id, (uint8_t) direction); + this->set_enrolling_(false); + this->defer([this]() { this->get_face_count_(); }); + break; + } + case HlkFm22xCommand::GET_STATUS: + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(data[2]); + } + this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); }); + break; + case HlkFm22xCommand::GET_VERSION: + if (this->version_text_sensor_ != nullptr) { + std::string version(data.begin() + 2, data.end()); + this->version_text_sensor_->publish_state(version); + } + this->defer([this]() { this->get_face_count_(); }); + break; + case HlkFm22xCommand::GET_ALL_FACE_IDS: + if (this->face_count_sensor_ != nullptr) { + this->face_count_sensor_->publish_state(data[2]); + } + break; + case HlkFm22xCommand::DELETE_FACE: + ESP_LOGI(TAG, "Deleted face"); + break; + case HlkFm22xCommand::DELETE_ALL_FACES: + ESP_LOGI(TAG, "Deleted all faces"); + break; + case HlkFm22xCommand::RESET: + ESP_LOGI(TAG, "Module reset"); + this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); + break; + default: + ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_); + break; + } +} + +void HlkFm22xComponent::set_enrolling_(bool enrolling) { + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(enrolling); + } +} + +void HlkFm22xComponent::dump_config() { + ESP_LOGCONFIG(TAG, "HLK_FM22X:"); + LOG_UPDATE_INTERVAL(this); + if (this->version_text_sensor_) { + LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str()); + } + if (this->enrolling_binary_sensor_) { + LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF"); + } + if (this->face_count_sensor_) { + LOG_SENSOR(" ", "Face Count", this->face_count_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state()); + } + if (this->status_sensor_) { + LOG_SENSOR(" ", "Status", this->status_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state()); + } + if (this->last_face_id_sensor_) { + LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state()); + } + if (this->last_face_name_text_sensor_) { + LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_); + ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str()); + } +} + +} // namespace esphome::hlk_fm22x diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h new file mode 100644 index 00000000000..5ecc715ea17 --- /dev/null +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -0,0 +1,224 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" + +#include +#include + +namespace esphome::hlk_fm22x { + +static const uint16_t START_CODE = 0xEFAA; +enum HlkFm22xCommand { + NONE = 0x00, + RESET = 0x10, + GET_STATUS = 0x11, + VERIFY = 0x12, + ENROLL = 0x13, + DELETE_FACE = 0x20, + DELETE_ALL_FACES = 0x21, + GET_ALL_FACE_IDS = 0x24, + GET_VERSION = 0x30, + GET_SERIAL_NUMBER = 0x93, +}; + +enum HlkFm22xResponseType { + REPLY = 0x00, + NOTE = 0x01, + IMAGE = 0x02, +}; + +enum HlkFm22xNoteType { + READY = 0x00, + FACE_STATE = 0x01, +}; + +enum HlkFm22xResult { + SUCCESS = 0x00, + REJECTED = 0x01, + ABORTED = 0x02, + FAILED4_CAMERA = 0x04, + FAILED4_UNKNOWNREASON = 0x05, + FAILED4_INVALIDPARAM = 0x06, + FAILED4_NOMEMORY = 0x07, + FAILED4_UNKNOWNUSER = 0x08, + FAILED4_MAXUSER = 0x09, + FAILED4_FACEENROLLED = 0x0A, + FAILED4_LIVENESSCHECK = 0x0C, + FAILED4_TIMEOUT = 0x0D, + FAILED4_AUTHORIZATION = 0x0E, + FAILED4_READ_FILE = 0x13, + FAILED4_WRITE_FILE = 0x14, + FAILED4_NO_ENCRYPT = 0x15, + FAILED4_NO_RGBIMAGE = 0x17, + FAILED4_JPGPHOTO_LARGE = 0x18, + FAILED4_JPGPHOTO_SMALL = 0x19, +}; + +enum HlkFm22xFaceDirection { + FACE_DIRECTION_UNDEFINED = 0x00, + FACE_DIRECTION_MIDDLE = 0x01, + FACE_DIRECTION_RIGHT = 0x02, + FACE_DIRECTION_LEFT = 0x04, + FACE_DIRECTION_DOWN = 0x08, + FACE_DIRECTION_UP = 0x10, +}; + +class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + + void set_face_count_sensor(sensor::Sensor *face_count_sensor) { this->face_count_sensor_ = face_count_sensor; } + void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; } + void set_last_face_id_sensor(sensor::Sensor *last_face_id_sensor) { + this->last_face_id_sensor_ = last_face_id_sensor; + } + void set_last_face_name_text_sensor(text_sensor::TextSensor *last_face_name_text_sensor) { + this->last_face_name_text_sensor_ = last_face_name_text_sensor; + } + void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { + this->enrolling_binary_sensor_ = enrolling_binary_sensor; + } + void set_version_text_sensor(text_sensor::TextSensor *version_text_sensor) { + this->version_text_sensor_ = version_text_sensor; + } + void add_on_face_scan_matched_callback(std::function callback) { + this->face_scan_matched_callback_.add(std::move(callback)); + } + void add_on_face_scan_unmatched_callback(std::function callback) { + this->face_scan_unmatched_callback_.add(std::move(callback)); + } + void add_on_face_scan_invalid_callback(std::function callback) { + this->face_scan_invalid_callback_.add(std::move(callback)); + } + void add_on_face_info_callback( + std::function callback) { + this->face_info_callback_.add(std::move(callback)); + } + void add_on_enrollment_done_callback(std::function callback) { + this->enrollment_done_callback_.add(std::move(callback)); + } + void add_on_enrollment_failed_callback(std::function callback) { + this->enrollment_failed_callback_.add(std::move(callback)); + } + + void enroll_face(const std::string &name, HlkFm22xFaceDirection direction); + void scan_face(); + void delete_face(int16_t face_id); + void delete_all_faces(); + void reset(); + + protected: + void get_face_count_(); + void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0); + void recv_command_(); + void handle_note_(const std::vector &data); + void handle_reply_(const std::vector &data); + void set_enrolling_(bool enrolling); + + HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE; + uint16_t wait_cycles_ = 0; + sensor::Sensor *face_count_sensor_{nullptr}; + sensor::Sensor *status_sensor_{nullptr}; + sensor::Sensor *last_face_id_sensor_{nullptr}; + binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; + text_sensor::TextSensor *last_face_name_text_sensor_{nullptr}; + text_sensor::TextSensor *version_text_sensor_{nullptr}; + CallbackManager face_scan_invalid_callback_; + CallbackManager face_scan_matched_callback_; + CallbackManager face_scan_unmatched_callback_; + CallbackManager face_info_callback_; + CallbackManager enrollment_done_callback_; + 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) + TEMPLATABLE_VALUE(uint8_t, direction) + + void play(Ts... x) override { + auto name = this->name_.value(x...); + auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...); + this->parent_->enroll_face(name, direction); + } +}; + +template class DeleteAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(int16_t, face_id) + + void play(Ts... x) override { + auto face_id = this->face_id_.value(x...); + this->parent_->delete_face(face_id); + } +}; + +template class DeleteAllAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->delete_all_faces(); } +}; + +template class ScanAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->scan_face(); } +}; + +template class ResetAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->reset(); } +}; + +} // namespace esphome::hlk_fm22x diff --git a/esphome/components/hlk_fm22x/sensor.py b/esphome/components/hlk_fm22x/sensor.py new file mode 100644 index 00000000000..e14b45599f1 --- /dev/null +++ b/esphome/components/hlk_fm22x/sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import CONF_STATUS, ENTITY_CATEGORY_DIAGNOSTIC, ICON_ACCOUNT + +from . import CONF_HLK_FM22X_ID, HlkFm22xComponent + +DEPENDENCIES = ["hlk_fm22x"] + +CONF_FACE_COUNT = "face_count" +CONF_LAST_FACE_ID = "last_face_id" +ICON_FACE = "mdi:face-recognition" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent), + cv.Optional(CONF_FACE_COUNT): sensor.sensor_schema( + icon=ICON_FACE, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_LAST_FACE_ID): sensor.sensor_schema( + icon=ICON_ACCOUNT, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_HLK_FM22X_ID]) + + for key in [ + CONF_FACE_COUNT, + CONF_STATUS, + CONF_LAST_FACE_ID, + ]: + if key not in config: + continue + conf = config[key] + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/hlk_fm22x/text_sensor.py b/esphome/components/hlk_fm22x/text_sensor.py new file mode 100644 index 00000000000..06da61c8b3a --- /dev/null +++ b/esphome/components/hlk_fm22x/text_sensor.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_VERSION, + ENTITY_CATEGORY_DIAGNOSTIC, + ICON_ACCOUNT, + ICON_RESTART, +) + +from . import CONF_HLK_FM22X_ID, HlkFm22xComponent + +DEPENDENCIES = ["hlk_fm22x"] + +CONF_LAST_FACE_NAME = "last_face_name" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent), + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + icon=ICON_RESTART, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_LAST_FACE_NAME): text_sensor.text_sensor_schema( + icon=ICON_ACCOUNT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_HLK_FM22X_ID]) + for key in [ + CONF_VERSION, + CONF_LAST_FACE_NAME, + ]: + if key not in config: + continue + conf = config[key] + sens = await text_sensor.new_text_sensor(conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index c1b47826a2d..aa01060d2c8 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -1,7 +1,7 @@ #pragma once - +#include #include "abstract_aqi_calculator.h" -// https://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.pdf +// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf namespace esphome { namespace hm3301 { @@ -16,16 +16,15 @@ class AQICalculator : public AbstractAQICalculator { } protected: - static const int AMOUNT_OF_LEVELS = 7; + static const int AMOUNT_OF_LEVELS = 6; - int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, - {201, 300}, {301, 400}, {401, 500}}; + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150}, - {151, 250}, {251, 350}, {351, 500}}; + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, + {56, 125}, {126, 225}, {226, INT_MAX}}; - int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, {255, 354}, - {355, 424}, {425, 504}, {505, 604}}; + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, INT_MAX}}; int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { int grid_index = get_grid_index_(value, array); diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index 0a91a2f63db..e72c5a21f5b 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -7,10 +7,8 @@ namespace homeassistant { static const char *const TAG = "homeassistant.time"; void HomeassistantTime::dump_config() { - ESP_LOGCONFIG(TAG, - "Home Assistant Time:\n" - " Timezone: '%s'", - this->timezone_.c_str()); + ESP_LOGCONFIG(TAG, "Home Assistant Time"); + RealTimeClock::dump_config(); } float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 482cd2da44e..8a82a44d7da 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -113,8 +113,8 @@ class HttpContainer : public Parented { class HttpRequestResponseTrigger : public Trigger, std::string &> { public: - void process(std::shared_ptr container, std::string &response_body) { - this->trigger(std::move(container), response_body); + void process(const std::shared_ptr &container, std::string &response_body) { + this->trigger(container, response_body); } }; @@ -210,7 +210,7 @@ template class HttpRequestSendAction : public Action { this->max_response_buffer_size_ = max_response_buffer_size; } - void play(Ts... x) override { + void play(const Ts &...x) override { std::string body; if (this->body_.has_value()) { body = this->body_.value(x...); diff --git a/esphome/components/http_request/ota/automation.h b/esphome/components/http_request/ota/automation.h index d4c21f1c721..6c50bb9b0d9 100644 --- a/esphome/components/http_request/ota/automation.h +++ b/esphome/components/http_request/ota/automation.h @@ -15,7 +15,7 @@ template class OtaHttpRequestComponentFlashAction : public Actio TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(std::string, username) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->md5_url_.has_value()) { this->parent_->set_md5_url(this->md5_url_.value(x...)); } diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index 9b3831b784c..277c6ca3e5f 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -41,7 +41,7 @@ template class SetHeaterLevelAction : public Action, publ public: TEMPLATABLE_VALUE(uint8_t, level) - void play(Ts... x) override { + void play(const Ts &...x) override { auto level = this->level_.value(x...); this->parent_->set_heater_level(level); @@ -52,7 +52,7 @@ template class SetHeaterAction : public Action, public Pa public: TEMPLATABLE_VALUE(bool, status) - void play(Ts... x) override { + void play(const Ts &...x) override { auto status = this->status_.value(x...); this->parent_->set_heater(status); diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 3cfec1e94dc..63089237590 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,11 +2,18 @@ import logging from esphome import pins import esphome.codegen as cg +from esphome.components.zephyr import ( + zephyr_add_overlay, + zephyr_add_prj_conf, + zephyr_data, +) +from esphome.components.zephyr.const import KEY_BOARD from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, CONF_FREQUENCY, + CONF_I2C, CONF_I2C_ID, CONF_ID, CONF_SCAN, @@ -15,10 +22,12 @@ from esphome.const import ( CONF_TIMEOUT, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_NRF52, PLATFORM_RP2040, PlatformFramework, ) from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.cpp_generator import MockObj import esphome.final_validate as fv LOGGER = logging.getLogger(__name__) @@ -28,6 +37,7 @@ I2CBus = i2c_ns.class_("I2CBus") InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus) ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component) IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component) +ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") @@ -41,6 +51,8 @@ def _bus_declare_type(value): return cv.declare_id(ArduinoI2CBus)(value) if CORE.using_esp_idf: return cv.declare_id(IDFI2CBus)(value) + if CORE.using_zephyr: + return cv.declare_id(ZephyrI2CBus)(value) raise NotImplementedError @@ -62,23 +74,70 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), - cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All( - cv.frequency, cv.Range(min=0, min_included=False) + cv.SplitDefault( + CONF_FREQUENCY, + esp32="50kHz", + esp8266="50kHz", + rp2040="50kHz", + nrf52="100kHz", + ): cv.All( + cv.frequency, + cv.Range(min=0, min_included=False), + ), + cv.Optional(CONF_TIMEOUT): cv.All( + cv.only_with_framework(["arduino", "esp-idf"]), + cv.positive_time_period, ), - cv.Optional(CONF_TIMEOUT): cv.positive_time_period, cv.Optional(CONF_SCAN, default=True): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA), - cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), + cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]), validate_config, ) +def _final_validate(config): + full_config = fv.full_config.get()[CONF_I2C] + if CORE.using_zephyr and len(full_config) > 1: + raise cv.Invalid("Second i2c is not implemented on Zephyr yet") + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.BUS) async def to_code(config): cg.add_global(i2c_ns.using) cg.add_define("USE_I2C") - var = cg.new_Pvariable(config[CONF_ID]) + if CORE.using_zephyr: + zephyr_add_prj_conf("I2C", True) + i2c = "i2c0" + if zephyr_data()[KEY_BOARD] in ["xiao_ble"]: + i2c = "i2c1" + zephyr_add_overlay( + f""" + &pinctrl {{ + {i2c}_default: {i2c}_default {{ + group1 {{ + psels = , + ; + }}; + }}; + {i2c}_sleep: {i2c}_sleep {{ + group1 {{ + psels = , + ; + low-power-enable; + }}; + }}; + }}; + """ + ) + var = cg.new_Pvariable( + config[CONF_ID], MockObj(f"DEVICE_DT_GET(DT_NODELABEL({i2c}))") + ) + else: + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) cg.add(var.set_sda_pin(config[CONF_SDA])) @@ -197,5 +256,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform( PlatformFramework.LN882X_ARDUINO, }, "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, + "i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR}, } ) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 31c21f398cb..f8c7a1b40bd 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -1,6 +1,7 @@ #include "i2c.h" #include "esphome/core/defines.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include @@ -23,6 +24,8 @@ void I2CBus::i2c_scan_() { } else if (err == ERROR_UNKNOWN) { scan_results_.emplace_back(address, false); } + // it takes 16sec to scan on nrf52. It prevents board reset. + arch_feed_wdt(); } #if defined(USE_ESP32) && defined(USE_LOGGER) esp_log_level_set("*", previous); diff --git a/esphome/components/i2c/i2c_bus_zephyr.cpp b/esphome/components/i2c/i2c_bus_zephyr.cpp new file mode 100644 index 00000000000..658dcee35ce --- /dev/null +++ b/esphome/components/i2c/i2c_bus_zephyr.cpp @@ -0,0 +1,133 @@ +#ifdef USE_ZEPHYR + +#include "i2c_bus_zephyr.h" +#include +#include "esphome/core/log.h" + +namespace esphome::i2c { + +static const char *const TAG = "i2c.zephyr"; + +void ZephyrI2CBus::setup() { + if (!device_is_ready(this->i2c_dev_)) { + ESP_LOGE(TAG, "I2C dev is not ready."); + mark_failed(); + return; + } + + int ret = i2c_configure(this->i2c_dev_, this->dev_config_); + if (ret < 0) { + ESP_LOGE(TAG, "I2C: Failed to configure device"); + } + + this->recovery_result_ = i2c_recover_bus(this->i2c_dev_); + if (this->recovery_result_ != 0) { + ESP_LOGE(TAG, "I2C recover bus failed, err %d", this->recovery_result_); + } + if (this->scan_) { + ESP_LOGV(TAG, "Scanning I2C bus for active devices..."); + this->i2c_scan_(); + } +} + +void ZephyrI2CBus::dump_config() { + auto get_speed = [](uint32_t dev_config) { + switch (I2C_SPEED_GET(dev_config)) { + case I2C_SPEED_STANDARD: + return "100 kHz"; + case I2C_SPEED_FAST: + return "400 kHz"; + case I2C_SPEED_FAST_PLUS: + return "1 MHz"; + case I2C_SPEED_HIGH: + return "3.4 MHz"; + case I2C_SPEED_ULTRA: + return "5 MHz"; + } + return "unknown"; + }; + ESP_LOGCONFIG(TAG, + "I2C Bus:\n" + " SDA Pin: GPIO%u\n" + " SCL Pin: GPIO%u\n" + " Frequency: %s\n" + " Name: %s", + this->sda_pin_, this->scl_pin_, get_speed(this->dev_config_), this->i2c_dev_->name); + + if (this->recovery_result_ != 0) { + ESP_LOGCONFIG(TAG, " Recovery: failed, err %d", this->recovery_result_); + } else { + ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered"); + } + if (this->scan_) { + ESP_LOGI(TAG, "Results from I2C bus scan:"); + if (scan_results_.empty()) { + ESP_LOGI(TAG, "Found no I2C devices!"); + } else { + for (const auto &s : scan_results_) { + if (s.second) { + ESP_LOGI(TAG, "Found I2C device at address 0x%02X", s.first); + } else { + ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first); + } + } + } + } +} + +ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { + if (!device_is_ready(this->i2c_dev_)) { + return ERROR_NOT_INITIALIZED; + } + + i2c_msg msgs[2]{}; + size_t cnt = 0; + uint8_t dst = 0x00; // dummy data to not use random value + + if (read_count == 0 && write_count == 0) { + msgs[cnt].buf = &dst; + msgs[cnt].len = 0U; + msgs[cnt++].flags = I2C_MSG_WRITE; + } else { + if (write_count) { + // the same struct is used for read/write — const cast is fine; data isn't modified + msgs[cnt].buf = const_cast(write_buffer); + msgs[cnt].len = write_count; + msgs[cnt++].flags = I2C_MSG_WRITE; + } + if (read_count) { + msgs[cnt].buf = const_cast(read_buffer); + msgs[cnt].len = read_count; + msgs[cnt++].flags = I2C_MSG_READ | I2C_MSG_RESTART; + } + } + + msgs[cnt - 1].flags |= I2C_MSG_STOP; + + auto err = i2c_transfer(this->i2c_dev_, msgs, cnt, address); + + if (err == -EIO) { + return ERROR_NOT_ACKNOWLEDGED; + } + + if (err != 0) { + ESP_LOGE(TAG, "i2c transfer error %d", err); + return ERROR_UNKNOWN; + } + + return ERROR_OK; +} + +void ZephyrI2CBus::set_frequency(uint32_t frequency) { + this->dev_config_ &= ~I2C_SPEED_MASK; + if (frequency >= 400000) { + this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_FAST); + } else { + this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_STANDARD); + } +} + +} // namespace esphome::i2c + +#endif diff --git a/esphome/components/i2c/i2c_bus_zephyr.h b/esphome/components/i2c/i2c_bus_zephyr.h new file mode 100644 index 00000000000..49cac5b9921 --- /dev/null +++ b/esphome/components/i2c/i2c_bus_zephyr.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef USE_ZEPHYR + +#include "i2c_bus.h" +#include "esphome/core/component.h" + +struct device; + +namespace esphome::i2c { + +class ZephyrI2CBus : public InternalI2CBus, public Component { + public: + explicit ZephyrI2CBus(const device *i2c_dev) : i2c_dev_(i2c_dev) {} + void setup() override; + void dump_config() override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_scan(bool scan) { scan_ = scan; } + void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; } + void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; } + void set_frequency(uint32_t frequency); + + int get_port() const override { return 0; } + + protected: + const device *i2c_dev_; + int recovery_result_ = 0; + uint8_t sda_pin_{}; + uint8_t scl_pin_{}; + uint32_t dev_config_{}; +}; + +} // namespace esphome::i2c + +#endif diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 9d080ea98e3..70260eeab3b 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -231,7 +231,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command this->connecting_sta_ = sta; wifi::global_wifi_component->set_sta(sta); - wifi::global_wifi_component->start_connecting(sta, false); + wifi::global_wifi_component->start_connecting(sta); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); diff --git a/esphome/components/inkplate/display.py b/esphome/components/inkplate/display.py index a0b0265cf16..89518dcfab9 100644 --- a/esphome/components/inkplate/display.py +++ b/esphome/components/inkplate/display.py @@ -20,8 +20,7 @@ import esphome.final_validate as fv from .const import INKPLATE_10_CUSTOM_WAVEFORMS, WAVEFORMS -DEPENDENCIES = ["i2c", "esp32"] -AUTO_LOAD = ["psram"] +DEPENDENCIES = ["i2c", "esp32", "psram"] CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index d9f2f5e50f0..f075d163fec 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -75,7 +75,7 @@ template class ResetAction : public Action { public: explicit ResetAction(IntegrationSensor *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset(); } + void play(const Ts &...x) override { this->parent_->reset(); } protected: IntegrationSensor *parent_; diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 35e8141ce54..735f3968097 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -52,11 +52,11 @@ class KeyCollector : public Component { }; template class EnableAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_enabled(true); } + void play(const Ts &...x) override { this->parent_->set_enabled(true); } }; template class DisableAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_enabled(false); } + void play(const Ts &...x) override { this->parent_->set_enabled(false); } }; } // namespace key_collector diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.h b/esphome/components/lcd_gpio/gpio_lcd_display.h index aba254a90a4..81e4dc51a0c 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.h +++ b/esphome/components/lcd_gpio/gpio_lcd_display.h @@ -2,13 +2,18 @@ #include "esphome/core/hal.h" #include "esphome/components/lcd_base/lcd_display.h" +#include "esphome/components/display/display.h" namespace esphome { namespace lcd_gpio { +class GPIOLCDDisplay; + +using gpio_lcd_writer_t = display::DisplayWriter; + class GPIOLCDDisplay : public lcd_base::LCDDisplay { public: - void set_writer(std::function &&writer) { this->writer_ = std::move(writer); } + void set_writer(gpio_lcd_writer_t &&writer) { this->writer_ = std::move(writer); } void setup() override; void set_data_pins(GPIOPin *d0, GPIOPin *d1, GPIOPin *d2, GPIOPin *d3) { this->data_pins_[0] = d0; @@ -43,7 +48,7 @@ class GPIOLCDDisplay : public lcd_base::LCDDisplay { GPIOPin *rw_pin_{nullptr}; GPIOPin *enable_pin_{nullptr}; GPIOPin *data_pins_[8]{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; - std::function writer_; + gpio_lcd_writer_t writer_; }; } // namespace lcd_gpio diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.h b/esphome/components/lcd_pcf8574/pcf8574_display.h index 4db3afb9b01..672b6090368 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.h +++ b/esphome/components/lcd_pcf8574/pcf8574_display.h @@ -3,13 +3,18 @@ #include "esphome/core/component.h" #include "esphome/components/lcd_base/lcd_display.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/display/display.h" namespace esphome { namespace lcd_pcf8574 { +class PCF8574LCDDisplay; + +using pcf8574_lcd_writer_t = display::DisplayWriter; + class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice { public: - void set_writer(std::function &&writer) { this->writer_ = std::move(writer); } + void set_writer(pcf8574_lcd_writer_t &&writer) { this->writer_ = std::move(writer); } void setup() override; void dump_config() override; void backlight(); @@ -24,7 +29,7 @@ class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice { // Stores the current state of the backlight. uint8_t backlight_value_; - std::function writer_; + pcf8574_lcd_writer_t writer_; }; } // namespace lcd_pcf8574 diff --git a/esphome/components/ld2410/automation.h b/esphome/components/ld2410/automation.h index 7cb9855f841..f4f1c197b2b 100644 --- a/esphome/components/ld2410/automation.h +++ b/esphome/components/ld2410/automation.h @@ -12,7 +12,7 @@ template class BluetoothPasswordSetAction : public Action explicit BluetoothPasswordSetAction(LD2410Component *ld2410_comp) : ld2410_comp_(ld2410_comp) {} TEMPLATABLE_VALUE(std::string, password) - void play(Ts... x) override { this->ld2410_comp_->set_bluetooth_password(this->password_.value(x...)); } + void play(const Ts &...x) override { this->ld2410_comp_->set_bluetooth_password(this->password_.value(x...)); } protected: LD2410Component *ld2410_comp_; diff --git a/esphome/components/ld2410/sensor.py b/esphome/components/ld2410/sensor.py index fca2b2ceca8..3bd34963bcc 100644 --- a/esphome/components/ld2410/sensor.py +++ b/esphome/components/ld2410/sensor.py @@ -31,35 +31,83 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, ), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), @@ -73,7 +121,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -81,7 +135,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, diff --git a/esphome/components/ld2412/sensor.py b/esphome/components/ld2412/sensor.py index abb823faad9..0bfbd9bf1d0 100644 --- a/esphome/components/ld2412/sensor.py +++ b/esphome/components/ld2412/sensor.py @@ -31,36 +31,84 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component), cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_LIGHT): sensor.sensor_schema( device_class=DEVICE_CLASS_ILLUMINANCE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_LIGHTBULB, unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor ), cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, ), cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema( device_class=DEVICE_CLASS_DISTANCE, - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_SIGNAL, unit_of_measurement=UNIT_CENTIMETER, ), cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( - filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}], + filters=[ + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, + ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, ), @@ -74,7 +122,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_MOTION_SENSOR, unit_of_measurement=UNIT_PERCENT, @@ -82,7 +136,13 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend( cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema( entity_category=ENTITY_CATEGORY_DIAGNOSTIC, filters=[ - {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)} + { + "timeout": { + "timeout": cv.TimePeriod(milliseconds=1000), + "value": "last", + } + }, + {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}, ], icon=ICON_FLASH, unit_of_measurement=UNIT_PERCENT, diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index b48c336d4ec..f544acc1124 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -174,7 +174,7 @@ static uint8_t calc_checksum(void *data, size_t size) { static int get_firmware_int(const char *version_string) { std::string version_str = version_string; if (version_str[0] == 'v') { - version_str = version_str.substr(1); + version_str.erase(0, 1); } version_str.erase(remove(version_str.begin(), version_str.end(), '.'), version_str.end()); int version_integer = stoi(version_str); diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index f04543bc5be..b24e3cfdb23 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -47,7 +47,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/libretiny_pwm/libretiny_pwm.h b/esphome/components/libretiny_pwm/libretiny_pwm.h index 42ce40ca392..f9117090548 100644 --- a/esphome/components/libretiny_pwm/libretiny_pwm.h +++ b/esphome/components/libretiny_pwm/libretiny_pwm.h @@ -40,7 +40,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(LibreTinyPWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 6e055741dad..8899db8bba5 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -15,7 +15,7 @@ template class ToggleAction : public Action { TEMPLATABLE_VALUE(uint32_t, transition_length) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->state_->toggle(); call.set_transition_length(this->transition_length_.optional_value(x...)); call.perform(); @@ -44,7 +44,7 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, warm_white) TEMPLATABLE_VALUE(std::string, effect) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); call.set_color_mode(this->color_mode_.optional_value(x...)); call.set_state(this->state_.optional_value(x...)); @@ -74,7 +74,7 @@ template class DimRelativeAction : public Action { TEMPLATABLE_VALUE(float, relative_brightness) TEMPLATABLE_VALUE(uint32_t, transition_length) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->parent_->make_call(); float rel = this->relative_brightness_.value(x...); float cur; @@ -107,7 +107,7 @@ template class DimRelativeAction : public Action { template class LightIsOnCondition : public Condition { public: explicit LightIsOnCondition(LightState *state) : state_(state) {} - bool check(Ts... x) override { return this->state_->current_values.is_on(); } + bool check(const Ts &...x) override { return this->state_->current_values.is_on(); } protected: LightState *state_; @@ -115,7 +115,7 @@ template class LightIsOnCondition : public Condition { template class LightIsOffCondition : public Condition { public: explicit LightIsOffCondition(LightState *state) : state_(state) {} - bool check(Ts... x) override { return !this->state_->current_values.is_on(); } + bool check(const Ts &...x) override { return !this->state_->current_values.is_on(); } protected: LightState *state_; @@ -179,7 +179,7 @@ template class AddressableSet : public Action { TEMPLATABLE_VALUE(float, blue) TEMPLATABLE_VALUE(float, white) - void play(Ts... x) override { + void play(const Ts &...x) override { auto *out = (AddressableLight *) this->parent_->get_output(); int32_t range_from = interpret_index(this->range_from_.value_or(x..., 0), out->size()); if (range_from < 0 || range_from >= out->size()) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index df17f53adce..b15ff84b978 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -52,8 +52,10 @@ static void log_invalid_parameter(const char *name, const LogString *message) { } static const LogString *color_mode_to_human(ColorMode color_mode) { - if (color_mode == ColorMode::UNKNOWN) - return LOG_STR("Unknown"); + if (color_mode == ColorMode::ON_OFF) + return LOG_STR("On/Off"); + if (color_mode == ColorMode::BRIGHTNESS) + return LOG_STR("Brightness"); if (color_mode == ColorMode::WHITE) return LOG_STR("White"); if (color_mode == ColorMode::COLOR_TEMPERATURE) @@ -68,7 +70,7 @@ static const LogString *color_mode_to_human(ColorMode color_mode) { return LOG_STR("RGB + cold/warm white"); if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) return LOG_STR("RGB + color temperature"); - return LOG_STR(""); + return LOG_STR("Unknown"); } // Helper to log percentage values @@ -406,7 +408,7 @@ void LightCall::transform_parameters_() { } } ColorMode LightCall::compute_color_mode_() { - const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes(); + auto supported_modes = this->parent_->get_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_state.cpp b/esphome/components/light/light_state.cpp index 7b0a698bb8f..b9e941c9c95 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,7 +1,8 @@ -#include "esphome/core/log.h" - -#include "light_output.h" #include "light_state.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" +#include "esphome/core/log.h" +#include "light_output.h" #include "transformers.h" namespace esphome { @@ -23,6 +24,9 @@ void LightState::setup() { effect->init_internal(this); } + // Start with loop disabled if idle - respects any effects/transitions set up during initialization + this->disable_loop_if_idle_(); + // When supported color temperature range is known, initialize color temperature setting within bounds. auto traits = this->get_traits(); float min_mireds = traits.get_min_mireds(); @@ -125,6 +129,9 @@ void LightState::loop() { this->is_transformer_active_ = false; this->transformer_ = nullptr; this->target_state_reached_callback_.call(); + + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } @@ -132,12 +139,19 @@ void LightState::loop() { if (this->next_write_) { this->next_write_ = false; this->output_->write_state(this); + // Disable loop if idle (no transformer and no effect) + this->disable_loop_if_idle_(); } } float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } -void LightState::publish_state() { this->remote_values_callback_.call(); } +void LightState::publish_state() { + this->remote_values_callback_.call(); +#if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_light_update(this); +#endif +} LightOutput *LightState::get_output() const { return this->output_; } @@ -222,6 +236,8 @@ void LightState::start_effect_(uint32_t effect_index) { this->active_effect_index_ = effect_index; auto *effect = this->get_active_effect_(); effect->start_internal(); + // Enable loop while effect is active + this->enable_loop(); } LightEffect *LightState::get_active_effect_() { if (this->active_effect_index_ == 0) { @@ -236,6 +252,8 @@ void LightState::stop_effect_() { effect->stop(); } this->active_effect_index_ = 0; + // Disable loop if idle (no effect and no transformer) + this->disable_loop_if_idle_(); } void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -245,6 +263,8 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng if (set_remote_values) { this->remote_values = target; } + // Enable loop while transition is active + this->enable_loop(); } void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) { @@ -260,6 +280,8 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b if (set_remote_values) { this->remote_values = target; }; + // Enable loop while flash is active + this->enable_loop(); } void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { @@ -271,6 +293,14 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot } this->output_->update_state(this); this->next_write_ = true; + this->enable_loop(); +} + +void LightState::disable_loop_if_idle_() { + // Only disable loop if both transformer and effect are inactive, and no pending writes + if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) { + this->disable_loop(); + } } void LightState::save_remote_values_() { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index bf63c0ec270..d6c985cfb90 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -256,6 +256,9 @@ class LightState : public EntityBase, public Component { /// Internal method to save the current remote_values to the preferences void save_remote_values_(); + /// Disable loop if neither transformer nor effect is active + void disable_loop_if_idle_(); + /// Store the output to allow effects to have more access. LightOutput *output_; /// The currently active transformer for this light (transition/flash). diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 294b0cad1de..c3bb27a9647 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -18,7 +18,8 @@ class LightTraits { public: LightTraits() = default; - const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; } + // Return by value to avoid dangling reference when get_traits() returns a temporary + ColorModeMask get_supported_color_modes() const { return this->supported_color_modes_; } void set_supported_color_modes(ColorModeMask supported_color_modes) { this->supported_color_modes_ = supported_color_modes; } diff --git a/esphome/components/lightwaverf/lightwaverf.h b/esphome/components/lightwaverf/lightwaverf.h index b9f2abfcb3c..ee4e91e9d1d 100644 --- a/esphome/components/lightwaverf/lightwaverf.h +++ b/esphome/components/lightwaverf/lightwaverf.h @@ -51,7 +51,7 @@ template class SendRawAction : public Action { void set_pulse_length(const int &data) { pulse_length_ = data; } void set_data(const std::vector &data) { code_ = data; } - void play(Ts... x) { + void play(const Ts &...x) { int repeats = this->repeat_.value(x...); int inverted = this->inverted_.value(x...); int pulse_length = this->pulse_length_.value(x...); diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h index 8cb3b64ffe6..0f596ef5e69 100644 --- a/esphome/components/lock/automation.h +++ b/esphome/components/lock/automation.h @@ -11,7 +11,7 @@ template class LockAction : public Action { public: explicit LockAction(Lock *a_lock) : lock_(a_lock) {} - void play(Ts... x) override { this->lock_->lock(); } + void play(const Ts &...x) override { this->lock_->lock(); } protected: Lock *lock_; @@ -21,7 +21,7 @@ template class UnlockAction : public Action { public: explicit UnlockAction(Lock *a_lock) : lock_(a_lock) {} - void play(Ts... x) override { this->lock_->unlock(); } + void play(const Ts &...x) override { this->lock_->unlock(); } protected: Lock *lock_; @@ -31,7 +31,7 @@ template class OpenAction : public Action { public: explicit OpenAction(Lock *a_lock) : lock_(a_lock) {} - void play(Ts... x) override { this->lock_->open(); } + void play(const Ts &...x) override { this->lock_->open(); } protected: Lock *lock_; @@ -40,7 +40,7 @@ template class OpenAction : public Action { template class LockCondition : public Condition { public: LockCondition(Lock *parent, bool state) : parent_(parent), state_(state) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { auto check_state = this->state_ ? LockState::LOCK_STATE_LOCKED : LockState::LOCK_STATE_UNLOCKED; return this->parent_->state == check_state; } diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index ddc54453496..54fefe8745e 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -1,4 +1,6 @@ #include "lock.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -53,6 +55,9 @@ void Lock::publish_state(LockState state) { this->rtc_.save(&this->state); ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state)); this->state_callback_.call(); +#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_lock_update(this); +#endif } void Lock::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index aa6935c5fc3..2a24f343c33 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -1,6 +1,8 @@ +import importlib import logging +import pkgutil -from esphome.automation import build_automation, register_action, validate_automation +from esphome.automation import build_automation, validate_automation import esphome.codegen as cg from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING from esphome.components.display import Display @@ -25,8 +27,8 @@ from esphome.cpp_generator import MockObj from esphome.final_validate import full_config from esphome.helpers import write_file_if_changed -from . import defines as df, helpers, lv_validation as lvalid -from .automation import disp_update, focused_widgets, refreshed_widgets, update_to_code +from . import defines as df, helpers, lv_validation as lvalid, widgets +from .automation import disp_update, focused_widgets, refreshed_widgets from .defines import add_define from .encoders import ( ENCODERS_CONFIG, @@ -41,14 +43,10 @@ from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent, lvgl_static from .schemas import ( DISP_BG_SCHEMA, - FLEX_OBJ_SCHEMA, FULL_STYLE_SCHEMA, - GRID_CELL_SCHEMA, - LAYOUT_SCHEMAS, WIDGET_TYPES, any_widget_schema, container_schema, - create_modify_schema, obj_schema, ) from .styles import add_top_layer, styles_to_code, theme_to_code @@ -57,7 +55,6 @@ from .trigger import add_on_boot_triggers, generate_triggers from .types import ( FontEngine, IdleTrigger, - ObjUpdateAction, PlainTrigger, lv_font_t, lv_group_t, @@ -72,32 +69,23 @@ from .widgets import ( set_obj_properties, styles_used, ) -from .widgets.animimg import animimg_spec -from .widgets.arc import arc_spec -from .widgets.button import button_spec -from .widgets.buttonmatrix import buttonmatrix_spec -from .widgets.canvas import canvas_spec -from .widgets.checkbox import checkbox_spec -from .widgets.dropdown import dropdown_spec -from .widgets.img import img_spec -from .widgets.keyboard import keyboard_spec -from .widgets.label import label_spec -from .widgets.led import led_spec -from .widgets.line import line_spec -from .widgets.lv_bar import bar_spec -from .widgets.meter import meter_spec + +# Import only what we actually use directly in this file from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code -from .widgets.obj import obj_spec -from .widgets.page import add_pages, generate_page_triggers, page_spec -from .widgets.qrcode import qr_code_spec -from .widgets.roller import roller_spec -from .widgets.slider import slider_spec -from .widgets.spinbox import spinbox_spec -from .widgets.spinner import spinner_spec -from .widgets.switch import switch_spec -from .widgets.tabview import tabview_spec -from .widgets.textarea import textarea_spec -from .widgets.tileview import tileview_spec +from .widgets.obj import obj_spec # Used in LVGL_SCHEMA +from .widgets.page import ( # page_spec used in LVGL_SCHEMA + add_pages, + generate_page_triggers, + page_spec, +) + +# Widget registration happens via WidgetType.__init__ in individual widget files +# The imports below trigger creation of the widget types +# Action registration (lvgl.{widget}.update) happens automatically +# in the WidgetType.__init__ method + +for module_info in pkgutil.iter_modules(widgets.__path__): + importlib.import_module(f".widgets.{module_info.name}", package=__package__) DOMAIN = "lvgl" DEPENDENCIES = ["display"] @@ -105,51 +93,6 @@ AUTO_LOAD = ["key_provider"] CODEOWNERS = ["@clydebarrow"] LOGGER = logging.getLogger(__name__) -for w_type in ( - label_spec, - obj_spec, - button_spec, - bar_spec, - slider_spec, - arc_spec, - line_spec, - spinner_spec, - led_spec, - animimg_spec, - checkbox_spec, - img_spec, - switch_spec, - tabview_spec, - buttonmatrix_spec, - meter_spec, - dropdown_spec, - roller_spec, - textarea_spec, - spinbox_spec, - keyboard_spec, - tileview_spec, - qr_code_spec, - canvas_spec, -): - WIDGET_TYPES[w_type.name] = w_type - -WIDGET_SCHEMA = any_widget_schema() - -LAYOUT_SCHEMAS[df.TYPE_GRID] = { - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA)) -} -LAYOUT_SCHEMAS[df.TYPE_FLEX] = { - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA)) -} -LAYOUT_SCHEMAS[df.TYPE_NONE] = { - cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema()) -} -for w_type in WIDGET_TYPES.values(): - register_action( - f"lvgl.{w_type.name}.update", - ObjUpdateAction, - create_modify_schema(w_type), - )(update_to_code) SIMPLE_TRIGGERS = ( df.CONF_ON_PAUSE, @@ -388,7 +331,7 @@ async def to_code(configs): # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") - if "transform_angle" in styles_used: + if {"transform_angle", "transform_zoom"} & styles_used: add_define("LV_COLOR_SCREEN_TRANSP", "1") for use in helpers.lv_uses: add_define(f"LV_USE_{use.upper()}") @@ -410,10 +353,19 @@ def display_schema(config): def add_hello_world(config): if df.CONF_WIDGETS not in config and CONF_PAGES not in config: LOGGER.info("No pages or widgets configured, creating default hello_world page") - config[df.CONF_WIDGETS] = cv.ensure_list(WIDGET_SCHEMA)(get_hello_world()) + config[df.CONF_WIDGETS] = any_widget_schema()(get_hello_world()) return 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() + } + )(value) + + FINAL_VALIDATE_SCHEMA = final_validation LVGL_SCHEMA = cv.All( @@ -450,6 +402,7 @@ LVGL_SCHEMA = cv.All( ), } ), + cv.Optional(CONF_PAGES): cv.ensure_list(container_schema(page_spec)), **{ cv.Optional(x): validate_automation( { @@ -459,24 +412,13 @@ LVGL_SCHEMA = cv.All( ) for x in SIMPLE_TRIGGERS }, - cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list( - WIDGET_SCHEMA - ), - cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( - container_schema(page_spec) - ), cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA), cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), cv.Optional( df.CONF_TRANSPARENCY_KEY, default=0x000400 ): lvalid.lv_color, - cv.Optional(df.CONF_THEME): cv.Schema( - { - cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) - for name, w in WIDGET_TYPES.items() - } - ), + cv.Optional(df.CONF_THEME): _theme_schema, cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA, cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema, cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG, diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 3241ba9c3f8..f2bcb6cc061 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -394,6 +394,8 @@ LV_FLEX_ALIGNMENTS = LvConstant( "SPACE_BETWEEN", ) +LV_FLEX_CROSS_ALIGNMENTS = LV_FLEX_ALIGNMENTS.extend("STRETCH") + LV_MENU_MODES = LvConstant( "LV_MENU_HEADER_", "TOP_FIXED", @@ -436,6 +438,7 @@ CONF_BUTTONS = "buttons" CONF_BYTE_ORDER = "byte_order" CONF_CHANGE_RATE = "change_rate" CONF_CLOSE_BUTTON = "close_button" +CONF_CONTAINER = "container" CONF_CONTROL = "control" CONF_DEFAULT_FONT = "default_font" CONF_DEFAULT_GROUP = "default_group" diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index 8d5b6354bb2..c2bd58f71c3 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -3,6 +3,8 @@ import re from esphome import config_validation as cv from esphome.const import CONF_ARGS, CONF_FORMAT +CONF_IF_NAN = "if_nan" + lv_uses = { "USER_DATA", "LOG", @@ -21,23 +23,48 @@ lv_fonts_used = set() esphome_fonts_used = set() lvgl_components_required = set() - -def validate_printf(value): - cfmt = r""" +# noqa +f_regex = re.compile( + r""" ( # start of capture group 1 % # literal "%" - (?:[-+0 #]{0,5}) # optional flags + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:h|l|ll|w|I|I32|I64)? # size + f # type + ) + """, + flags=re.VERBOSE, +) +# noqa +c_regex = re.compile( + r""" + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags (?:\d+|\*)? # width (?:\.(?:\d+|\*))? # precision (?:h|l|ll|w|I|I32|I64)? # size [cCdiouxXeEfgGaAnpsSZ] # type ) - """ # noqa - matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE) + """, + flags=re.VERBOSE, +) + + +def validate_printf(value): + format_string = value[CONF_FORMAT] + matches = c_regex.findall(format_string) if len(matches) != len(value[CONF_ARGS]): raise cv.Invalid( f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" ) + + if value.get(CONF_IF_NAN) and len(f_regex.findall(format_string)) != 1: + raise cv.Invalid( + "Use of 'if_nan' requires a single valid printf-pattern of type %f" + ) return value diff --git a/esphome/components/lvgl/layout.py b/esphome/components/lvgl/layout.py new file mode 100644 index 00000000000..a6aa816fda6 --- /dev/null +++ b/esphome/components/lvgl/layout.py @@ -0,0 +1,378 @@ +import re +import textwrap + +import esphome.config_validation as cv +from esphome.const import CONF_HEIGHT, CONF_TYPE, CONF_WIDTH + +from .defines import ( + CONF_FLEX_ALIGN_CROSS, + CONF_FLEX_ALIGN_MAIN, + CONF_FLEX_ALIGN_TRACK, + CONF_FLEX_FLOW, + CONF_FLEX_GROW, + CONF_GRID_CELL_COLUMN_POS, + CONF_GRID_CELL_COLUMN_SPAN, + CONF_GRID_CELL_ROW_POS, + CONF_GRID_CELL_ROW_SPAN, + CONF_GRID_CELL_X_ALIGN, + CONF_GRID_CELL_Y_ALIGN, + CONF_GRID_COLUMN_ALIGN, + CONF_GRID_COLUMNS, + CONF_GRID_ROW_ALIGN, + CONF_GRID_ROWS, + CONF_LAYOUT, + CONF_PAD_COLUMN, + CONF_PAD_ROW, + CONF_WIDGETS, + FLEX_FLOWS, + LV_CELL_ALIGNMENTS, + LV_FLEX_ALIGNMENTS, + LV_FLEX_CROSS_ALIGNMENTS, + LV_GRID_ALIGNMENTS, + TYPE_FLEX, + TYPE_GRID, + TYPE_NONE, + LvConstant, +) +from .lv_validation import padding, size + +cell_alignments = LV_CELL_ALIGNMENTS.one_of +grid_alignments = LV_GRID_ALIGNMENTS.one_of +flex_alignments = LV_FLEX_ALIGNMENTS.one_of + +FLEX_LAYOUT_SCHEMA = { + cv.Required(CONF_TYPE): cv.one_of(TYPE_FLEX, lower=True), + cv.Optional(CONF_FLEX_FLOW, default="row_wrap"): FLEX_FLOWS.one_of, + cv.Optional(CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, + cv.Optional( + CONF_FLEX_ALIGN_CROSS, default="start" + ): LV_FLEX_CROSS_ALIGNMENTS.one_of, + cv.Optional(CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, + cv.Optional(CONF_PAD_ROW): padding, + cv.Optional(CONF_PAD_COLUMN): padding, + cv.Optional(CONF_FLEX_GROW): cv.int_, +} + +FLEX_HV_STYLE = { + CONF_FLEX_ALIGN_MAIN: "LV_FLEX_ALIGN_SPACE_EVENLY", + CONF_FLEX_ALIGN_TRACK: "LV_FLEX_ALIGN_CENTER", + CONF_FLEX_ALIGN_CROSS: "LV_FLEX_ALIGN_CENTER", + CONF_TYPE: TYPE_FLEX, +} + +FLEX_OBJ_SCHEMA = { + cv.Optional(CONF_FLEX_GROW): cv.int_, +} + + +def flex_hv_schema(dir): + dir = CONF_HEIGHT if dir == "horizontal" else CONF_WIDTH + return { + cv.Optional(CONF_FLEX_GROW, default=1): cv.int_, + cv.Optional(dir, default="100%"): size, + } + + +def grid_free_space(value): + value = cv.Upper(value) + if value.startswith("FR(") and value.endswith(")"): + value = value.removesuffix(")").removeprefix("FR(") + return f"LV_GRID_FR({cv.positive_int(value)})" + raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") + + +grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space) + +GRID_CELL_SCHEMA = { + cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, +} + + +class Layout: + """ + Define properties for a layout + The base class is layout "none" + """ + + def get_type(self): + return TYPE_NONE + + def get_layout_schemas(self, config: dict) -> tuple: + """ + Get the layout and child schema for a given widget based on its layout type. + """ + return None, {} + + def validate(self, config): + """ + Validate the layout configuration. This is called late in the schema validation + :param config: The input configuration + :return: The validated configuration + """ + return config + + +class FlexLayout(Layout): + def get_type(self): + return TYPE_FLEX + + def get_layout_schemas(self, config: dict) -> tuple: + layout = config.get(CONF_LAYOUT) + if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_FLEX: + return None, {} + child_schema = FLEX_OBJ_SCHEMA + if grow := layout.get(CONF_FLEX_GROW): + child_schema = {cv.Optional(CONF_FLEX_GROW, default=grow): cv.int_} + # Polyfill to implement stretch alignment for flex containers + # LVGL does not support this natively, so we add a 100% size property to the children in the cross-axis + if layout.get(CONF_FLEX_ALIGN_CROSS) == "LV_FLEX_ALIGN_STRETCH": + dimension = ( + CONF_WIDTH + if "COLUMN" in layout[CONF_FLEX_FLOW].upper() + else CONF_HEIGHT + ) + child_schema[cv.Optional(dimension, default="100%")] = size + return FLEX_LAYOUT_SCHEMA, child_schema + + def validate(self, config): + """ + Perform validation on the container and its children for this layout + :param config: + :return: + """ + return config + + +class DirectionalLayout(FlexLayout): + def __init__(self, direction: str, flow): + """ + :param direction: "horizontal" or "vertical" + :param flow: "row" or "column" + """ + super().__init__() + self.direction = direction + self.flow = flow + + def get_type(self): + return self.direction + + def get_layout_schemas(self, config: dict) -> tuple: + if not isinstance(config.get(CONF_LAYOUT), str): + return None, {} + if config.get(CONF_LAYOUT, "").lower() != self.direction: + return None, {} + return cv.one_of(self.direction, lower=True), flex_hv_schema(self.direction) + + def validate(self, config): + assert config[CONF_LAYOUT].lower() == self.direction + config[CONF_LAYOUT] = { + **FLEX_HV_STYLE, + CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(), + } + return config + + +class GridLayout(Layout): + _GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$") + + def get_type(self): + return TYPE_GRID + + def get_layout_schemas(self, config: dict) -> tuple: + layout = config.get(CONF_LAYOUT) + if isinstance(layout, str): + if GridLayout._GRID_LAYOUT_REGEX.match(layout): + return ( + cv.string, + { + cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional( + CONF_GRID_CELL_ROW_SPAN, default=1 + ): cv.positive_int, + cv.Optional( + CONF_GRID_CELL_COLUMN_SPAN, default=1 + ): cv.positive_int, + cv.Optional( + CONF_GRID_CELL_X_ALIGN, default="center" + ): grid_alignments, + cv.Optional( + CONF_GRID_CELL_Y_ALIGN, default="center" + ): grid_alignments, + }, + ) + # Not a valid grid layout string + return None, {} + + if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_GRID: + return None, {} + return ( + { + cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True), + cv.Required(CONF_GRID_ROWS): [grid_spec], + cv.Required(CONF_GRID_COLUMNS): [grid_spec], + cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments, + cv.Optional(CONF_PAD_ROW): padding, + cv.Optional(CONF_PAD_COLUMN): padding, + }, + { + cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int, + cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, + cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments, + cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments, + }, + ) + + def validate(self, config: dict): + """ + Validate the grid layout. + The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns". + Either all cells must have a row and column, + or none, in which case the grid layout is auto-generated. + :param config: + :return: The config updated with auto-generated values + """ + layout = config.get(CONF_LAYOUT) + if isinstance(layout, str): + # If the layout is a string, assume it is in the format "rows x columns", implying + # a grid layout with the specified number of rows and columns each with CONTENT sizing. + layout = layout.strip() + match = GridLayout._GRID_LAYOUT_REGEX.match(layout) + if match: + rows = int(match.group(1)) + cols = int(match.group(2)) + layout = { + CONF_TYPE: TYPE_GRID, + CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows, + CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols, + } + config[CONF_LAYOUT] = layout + else: + raise cv.Invalid( + f"Invalid grid layout format: {config}, expected 'rows x columns'", + [CONF_LAYOUT], + ) + # should be guaranteed to be a dict at this point + assert isinstance(layout, dict) + assert layout.get(CONF_TYPE).lower() == TYPE_GRID + rows = len(layout[CONF_GRID_ROWS]) + columns = len(layout[CONF_GRID_COLUMNS]) + used_cells = [[None] * columns for _ in range(rows)] + for index, widget in enumerate(config.get(CONF_WIDGETS, [])): + _, w = next(iter(widget.items())) + if (CONF_GRID_CELL_COLUMN_POS in w) != (CONF_GRID_CELL_ROW_POS in w): + raise cv.Invalid( + "Both row and column positions must be specified, or both omitted", + [CONF_WIDGETS, index], + ) + if CONF_GRID_CELL_ROW_POS in w: + row = w[CONF_GRID_CELL_ROW_POS] + column = w[CONF_GRID_CELL_COLUMN_POS] + else: + try: + row, column = next( + (r_idx, c_idx) + for r_idx, row in enumerate(used_cells) + for c_idx, value in enumerate(row) + if value is None + ) + except StopIteration: + raise cv.Invalid( + "No free cells available in grid layout", [CONF_WIDGETS, index] + ) from None + w[CONF_GRID_CELL_ROW_POS] = row + w[CONF_GRID_CELL_COLUMN_POS] = column + + for i in range(w[CONF_GRID_CELL_ROW_SPAN]): + for j in range(w[CONF_GRID_CELL_COLUMN_SPAN]): + if row + i >= rows or column + j >= columns: + raise cv.Invalid( + f"Cell at {row}/{column} span {w[CONF_GRID_CELL_ROW_SPAN]}x{w[CONF_GRID_CELL_COLUMN_SPAN]} " + f"exceeds grid size {rows}x{columns}", + [CONF_WIDGETS, index], + ) + if used_cells[row + i][column + j] is not None: + raise cv.Invalid( + f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", + [CONF_WIDGETS, index], + ) + used_cells[row + i][column + j] = index + + return config + + +LAYOUT_CLASSES = ( + FlexLayout(), + GridLayout(), + DirectionalLayout("horizontal", "row"), + DirectionalLayout("vertical", "column"), +) +LAYOUT_CHOICES = [x.get_type() for x in LAYOUT_CLASSES] + + +def append_layout_schema(schema, config: dict): + """ + Get the child layout schema for a given widget based on its layout type. + :param config: The config to check + :return: A schema for the layout including a widgets key + """ + # Local import to avoid circular dependencies + if CONF_WIDGETS not in config: + if CONF_LAYOUT in config: + raise cv.Invalid( + f"Layout {config[CONF_LAYOUT]} requires a {CONF_WIDGETS} key", + [CONF_LAYOUT], + ) + return schema + + from .schemas import any_widget_schema + + if CONF_LAYOUT not in config: + # If no layout is specified, return the schema as is + return schema.extend({cv.Optional(CONF_WIDGETS): any_widget_schema()}) + layout = config[CONF_LAYOUT] + # Sanity check the layout to avoid redundant checks in each type + if not isinstance(layout, str) and not isinstance(layout, dict): + raise cv.Invalid( + "The 'layout' option must be a string or a dictionary", [CONF_LAYOUT] + ) + if isinstance(layout, dict) and not isinstance(layout.get(CONF_TYPE), str): + raise cv.Invalid( + "Invalid layout type; must be a string ('flex' or 'grid')", + [CONF_LAYOUT, CONF_TYPE], + ) + + for layout_class in LAYOUT_CLASSES: + layout_schema, child_schema = layout_class.get_layout_schemas(config) + if layout_schema: + layout_schema = cv.Schema( + { + cv.Required(CONF_LAYOUT): layout_schema, + cv.Required(CONF_WIDGETS): any_widget_schema(child_schema), + } + ) + layout_schema.add_extra(layout_class.validate) + return layout_schema.extend(schema) + + if isinstance(layout, dict): + raise cv.Invalid( + "Invalid layout type; must be 'flex' or 'grid'", [CONF_LAYOUT, CONF_TYPE] + ) + raise cv.Invalid( + textwrap.dedent( + """ + Invalid 'layout' value + layout choices are 'horizontal', 'vertical', 'x', + or a dictionary with a 'type' key + """ + ), + [CONF_LAYOUT], + ) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 6f95a32a186..045258555c6 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,3 +1,4 @@ +import re from typing import TYPE_CHECKING, Any import esphome.codegen as cg @@ -32,7 +33,13 @@ from .defines import ( call_lambda, literal, ) -from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component +from .helpers import ( + CONF_IF_NAN, + add_lv_use, + esphome_fonts_used, + lv_fonts_used, + requires_component, +) from .types import lv_font_t, lv_gradient_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -246,6 +253,8 @@ def pixels_or_percent_validator(value): return ["pixels", "..%"] if isinstance(value, str) and value.lower().endswith("px"): value = cv.int_(value[:-2]) + if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value): + return value value = cv.Any(cv.int_, cv.percentage)(value) if isinstance(value, int): return value @@ -409,7 +418,13 @@ class TextValidator(LValidator): str_args = [str(x) for x in value[CONF_ARGS]] arg_expr = cg.RawExpression(",".join(str_args)) format_str = cpp_string_escape(format_str) - return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()" + if nanval := value.get(CONF_IF_NAN): + nanval = cpp_string_escape(nanval) + return literal( + f"(std::isfinite({arg_expr}) ? {sprintf_str} : {nanval})" + ) + return literal(sprintf_str) if time_format := value.get(CONF_TIME_FORMAT): source = value[CONF_TIME] if isinstance(source, Lambda): diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index ea38845c078..c11597131f1 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -299,6 +299,7 @@ class LvExpr(MockLv): # Top level mock for generic lv_ calls to be recorded lv = MockLv("lv_") +LV = MockLv("LV_") # Just generate an expression lv_expr = LvExpr("lv_") # Mock for lv_obj_ calls @@ -327,7 +328,7 @@ def lv_assign(target, expression): lv_add(AssignmentExpression("", "", target, expression)) -def lv_Pvariable(type, name): +def lv_Pvariable(type, name) -> MockObj: """ Create but do not initialise a pointer variable :param type: Type of the variable target @@ -343,7 +344,7 @@ def lv_Pvariable(type, name): return var -def lv_variable(type, name): +def lv_variable(type, name) -> MockObj: """ Create but do not initialise a variable :param type: Type of the variable target diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 947342089cd..05005b02170 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -171,6 +171,7 @@ bool LvPageType::is_showing() const { return this->parent_->get_current_page() = void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); + auto height_rounded = (height + this->draw_rounding - 1) / this->draw_rounding * this->draw_rounding; auto x1 = area->x1; auto y1 = area->y1; lv_color_t *dst = this->rotate_buf_; @@ -178,13 +179,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { 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 + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } y1 = x1; x1 = this->disp_drv_.ver_res - area->y1 - height; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; case display::DISPLAY_ROTATION_180_DEGREES: @@ -200,13 +201,13 @@ void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *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 + x] = *ptr++; + dst[y * height_rounded + x] = *ptr++; } } x1 = y1; y1 = this->disp_drv_.hor_res - area->x1 - width; - width = height; - height = lv_area_get_width(area); + height = width; + width = height_rounded; break; default: @@ -443,8 +444,10 @@ LvglComponent::LvglComponent(std::vector displays, float buf void LvglComponent::setup() { auto *display = this->displays_[0]; - auto width = display->get_width(); - auto height = display->get_height(); + auto rounding = this->draw_rounding; + // cater for displays with dimensions that don't divide by the required rounding + auto width = (display->get_width() + rounding - 1) / rounding * rounding; + auto height = (display->get_height() + rounding - 1) / rounding * rounding; auto frac = this->buffer_frac_; if (frac == 0) frac = 1; @@ -469,9 +472,8 @@ void LvglComponent::setup() { } this->buffer_frac_ = frac; lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels); - this->disp_drv_.hor_res = width; - this->disp_drv_.ver_res = height; - // this->setup_driver_(display->get_width(), display->get_height()); + this->disp_drv_.hor_res = display->get_width(); + this->disp_drv_.ver_res = display->get_height(); lv_disp_drv_update(this->disp_, &this->disp_drv_); this->rotation = display->get_rotation(); if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) { diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 50d192fde34..1ae05f933f1 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -129,7 +129,7 @@ template class ObjUpdateAction : public Action { public: explicit ObjUpdateAction(std::function &&lamb) : lamb_(std::move(lamb)) {} - void play(Ts... x) override { this->lamb_(x...); } + void play(const Ts &...x) override { this->lamb_(x...); } protected: std::function lamb_; @@ -263,7 +263,7 @@ class IdleTrigger : public Trigger<> { template class LvglAction : public Action, public Parented { public: explicit LvglAction(std::function &&lamb) : action_(std::move(lamb)) {} - void play(Ts... x) override { this->action_(this->parent_); } + void play(const Ts &...x) override { this->action_(this->parent_); } protected: std::function action_{}; @@ -272,7 +272,7 @@ template class LvglAction : public Action, public Parente template class LvglCondition : public Condition, public Parented { public: LvglCondition(std::function &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {} - bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } + bool check(const Ts &...x) override { return this->condition_lambda_(this->parent_); } protected: std::function condition_lambda_{}; diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 959d203c414..6b77f66abb5 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -12,17 +12,21 @@ from esphome.const import ( CONF_TEXT, CONF_TIME, CONF_TRIGGER_ID, - CONF_TYPE, CONF_X, CONF_Y, ) from esphome.core import TimePeriod from esphome.core.config import StartupTrigger -from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid -from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID -from .helpers import add_lv_use, requires_component, validate_printf +from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR +from .helpers import CONF_IF_NAN, requires_component, validate_printf +from .layout import ( + FLEX_OBJ_SCHEMA, + GRID_CELL_SCHEMA, + append_layout_schema, + grid_alignments, +) from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity from .lvcode import LvglComponent, lv_event_t_ptr from .types import ( @@ -50,6 +54,7 @@ PRINTF_TEXT_SCHEMA = cv.All( { cv.Required(CONF_FORMAT): cv.string, cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_), + cv.Optional(CONF_IF_NAN): cv.string, }, ), validate_printf, @@ -72,11 +77,9 @@ def _validate_text(value): # A schema for text properties -TEXT_SCHEMA = cv.Schema( - { - cv.Optional(CONF_TEXT): _validate_text, - } -) +TEXT_SCHEMA = { + cv.Optional(CONF_TEXT): _validate_text, +} LIST_ACTION_SCHEMA = cv.ensure_list( cv.maybe_simple_value( @@ -136,7 +139,7 @@ STYLE_PROPS = { "arc_opa": lvalid.opacity, "arc_color": lvalid.lv_color, "arc_rounded": lvalid.lv_bool, - "arc_width": lvalid.lv_positive_int, + "arc_width": lvalid.pixels, "anim_time": lvalid.lv_milliseconds, "bg_color": lvalid.lv_color, "bg_grad": lv_gradient, @@ -223,10 +226,6 @@ STYLE_REMAP = { "image_recolor_opa": "img_recolor_opa", } -cell_alignments = df.LV_CELL_ALIGNMENTS.one_of -grid_alignments = df.LV_GRID_ALIGNMENTS.one_of -flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of - # Complete object style schema STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( { @@ -266,10 +265,8 @@ def part_schema(parts): :param parts: The parts to include :return: The schema """ - return ( - cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}) - .extend(STATE_SCHEMA) - .extend(FLAG_SCHEMA) + return STATE_SCHEMA.extend(FLAG_SCHEMA).extend( + {cv.Optional(part): STATE_SCHEMA for part in parts} ) @@ -277,10 +274,10 @@ def automation_schema(typ: LvType): events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS if typ.has_on_value: events = events + (CONF_ON_VALUE,) - args = typ.get_arg_type() if isinstance(typ, LvType) else [] + args = typ.get_arg_type() args.append(lv_event_t_ptr) - return cv.Schema( - { + return { + **{ cv.Optional(event): validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -289,14 +286,11 @@ def automation_schema(typ: LvType): } ) for event in events - } - ).extend( - { - cv.Optional(CONF_ON_BOOT): validate_automation( - {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)} - ) - } - ) + }, + cv.Optional(CONF_ON_BOOT): validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)} + ), + } def base_update_schema(widget_type, parts): @@ -335,75 +329,17 @@ def obj_schema(widget_type: WidgetType): """ return ( part_schema(widget_type.parts) - .extend(LAYOUT_SCHEMA) .extend(ALIGN_TO_SCHEMA) .extend(automation_schema(widget_type.w_type)) .extend( - cv.Schema( - { - cv.Optional(CONF_STATE): SET_STATE_SCHEMA, - cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), - } - ) + { + cv.Optional(CONF_STATE): SET_STATE_SCHEMA, + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), + } ) ) -def _validate_grid_layout(config): - layout = config[df.CONF_LAYOUT] - rows = len(layout[df.CONF_GRID_ROWS]) - columns = len(layout[df.CONF_GRID_COLUMNS]) - used_cells = [[None] * columns for _ in range(rows)] - for index, widget in enumerate(config[df.CONF_WIDGETS]): - _, w = next(iter(widget.items())) - if (df.CONF_GRID_CELL_COLUMN_POS in w) != (df.CONF_GRID_CELL_ROW_POS in w): - # pylint: disable=raise-missing-from - raise cv.Invalid( - "Both row and column positions must be specified, or both omitted", - [df.CONF_WIDGETS, index], - ) - if df.CONF_GRID_CELL_ROW_POS in w: - row = w[df.CONF_GRID_CELL_ROW_POS] - column = w[df.CONF_GRID_CELL_COLUMN_POS] - else: - try: - row, column = next( - (r_idx, c_idx) - for r_idx, row in enumerate(used_cells) - for c_idx, value in enumerate(row) - if value is None - ) - except StopIteration: - # pylint: disable=raise-missing-from - raise cv.Invalid( - "No free cells available in grid layout", [df.CONF_WIDGETS, index] - ) - w[df.CONF_GRID_CELL_ROW_POS] = row - w[df.CONF_GRID_CELL_COLUMN_POS] = column - - for i in range(w[df.CONF_GRID_CELL_ROW_SPAN]): - for j in range(w[df.CONF_GRID_CELL_COLUMN_SPAN]): - if row + i >= rows or column + j >= columns: - # pylint: disable=raise-missing-from - raise cv.Invalid( - f"Cell at {row}/{column} span {w[df.CONF_GRID_CELL_ROW_SPAN]}x{w[df.CONF_GRID_CELL_COLUMN_SPAN]} " - f"exceeds grid size {rows}x{columns}", - [df.CONF_WIDGETS, index], - ) - if used_cells[row + i][column + j] is not None: - # pylint: disable=raise-missing-from - raise cv.Invalid( - f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", - [df.CONF_WIDGETS, index], - ) - used_cells[row + i][column + j] = index - - return config - - -LAYOUT_SCHEMAS = {} -LAYOUT_VALIDATORS = {TYPE_GRID: _validate_grid_layout} - ALIGN_TO_SCHEMA = { cv.Optional(df.CONF_ALIGN_TO): cv.Schema( { @@ -416,57 +352,6 @@ ALIGN_TO_SCHEMA = { } -def grid_free_space(value): - value = cv.Upper(value) - if value.startswith("FR(") and value.endswith(")"): - value = value.removesuffix(")").removeprefix("FR(") - return f"LV_GRID_FR({cv.positive_int(value)})" - raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)") - - -grid_spec = cv.Any( - lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space -) - -LAYOUT_SCHEMA = { - cv.Optional(df.CONF_LAYOUT): cv.typed_schema( - { - df.TYPE_GRID: { - cv.Required(df.CONF_GRID_ROWS): [grid_spec], - cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], - cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, - cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, - cv.Optional(df.CONF_PAD_ROW): lvalid.padding, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, - }, - df.TYPE_FLEX: { - cv.Optional( - df.CONF_FLEX_FLOW, default="row_wrap" - ): df.FLEX_FLOWS.one_of, - cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, - cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, - cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, - cv.Optional(df.CONF_PAD_ROW): lvalid.padding, - cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding, - }, - }, - lower=True, - ) -} - -GRID_CELL_SCHEMA = { - cv.Optional(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, - cv.Optional(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, - cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, - cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, - cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, - cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments, -} - -FLEX_OBJ_SCHEMA = { - cv.Optional(df.CONF_FLEX_GROW): cv.int_, -} - DISP_BG_SCHEMA = cv.Schema( { cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any( @@ -498,48 +383,11 @@ ALL_STYLES = { } -def container_validator(schema, widget_type: WidgetType): - """ - Create a validator for a container given the widget type - :param schema: Base schema to extend - :param widget_type: - :return: - """ - - def validator(value): - if w_sch := widget_type.schema: - if isinstance(w_sch, dict): - w_sch = cv.Schema(w_sch) - # order is important here to preserve extras - result = w_sch.extend(schema) - else: - result = schema - ltype = df.TYPE_NONE - if value and (layout := value.get(df.CONF_LAYOUT)): - if not isinstance(layout, dict): - raise cv.Invalid("Layout value must be a dict") - ltype = layout.get(CONF_TYPE) - if not ltype: - raise (cv.Invalid("Layout schema requires type:")) - add_lv_use(ltype) - if value == SCHEMA_EXTRACT: - return result - result = result.extend( - LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) - ) - value = result(value) - if layout_validator := LAYOUT_VALIDATORS.get(ltype): - value = layout_validator(value) - return value - - return validator - - def container_schema(widget_type: WidgetType, extras=None): """ Create a schema for a container widget of a given type. All obj properties are available, plus the extras passed in, plus any defined for the specific widget being specified. - :param widget_type: The widget type, e.g. "img" + :param widget_type: The widget type, e.g. "image" :param extras: Additional options to be made available, e.g. layout properties for children :return: The schema for this type of widget. """ @@ -549,31 +397,53 @@ def container_schema(widget_type: WidgetType, extras=None): if extras: schema = schema.extend(extras) # Delayed evaluation for recursion - return container_validator(schema, widget_type) + schema = schema.extend(widget_type.schema) -def widget_schema(widget_type: WidgetType, extras=None): - """ - Create a schema for a given widget type - :param widget_type: The name of the widget - :param extras: - :return: - """ - validator = container_schema(widget_type, extras=extras) - if required := widget_type.required_component: - validator = cv.All(validator, requires_component(required)) - return cv.Exclusive(widget_type.name, df.CONF_WIDGETS), validator + def validator(value): + return append_layout_schema(schema, value)(value) - -# All widget schemas must be defined before this is called. + return validator def any_widget_schema(extras=None): """ - Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of + Dynamically generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of widget under the widgets: key. + This uses lazy evaluation - the schema is built when called during validation, + not at import time. This allows external components to register widgets + before schema validation begins. + :param extras: Additional schema to be applied to each generated one - :return: + :return: A validator for the Widgets key """ - return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_TYPES.values())) + + def validator(value): + if isinstance(value, dict): + # Convert to list + value = [{k: v} for k, v in value.items()] + if not isinstance(value, list): + raise cv.Invalid("Expected a list of widgets") + result = [] + for index, entry in enumerate(value): + if not isinstance(entry, dict) or len(entry) != 1: + raise cv.Invalid( + "Each widget must be a dictionary with a single key", path=[index] + ) + [(key, value)] = entry.items() + # Validate the widget against its schema + widget_type = WIDGET_TYPES.get(key) + if not widget_type: + raise cv.Invalid(f"Unknown widget type: {key}", path=[index]) + container_validator = container_schema(widget_type, extras=extras) + if required := widget_type.required_component: + container_validator = cv.All( + container_validator, requires_component(required) + ) + # Apply custom validation + value = widget_type.validate(value or {}) + result.append({key: container_validator(value)}) + return result + + return validator diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 3b1fd67d68f..70bb3e7bcbe 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -41,16 +41,16 @@ class LVGLSelect : public select::Select, public Component { } void publish() { - this->publish_state(this->widget_->get_selected_text()); + auto index = this->widget_->get_selected_index(); + this->publish_state(index); if (this->restore_) { - auto index = this->widget_->get_selected_index(); this->pref_.save(&index); } } protected: - void control(const std::string &value) override { - this->widget_->set_selected_text(value, this->anim_); + void control(size_t index) override { + this->widget_->set_selected_index(index, this->anim_); this->publish(); } void set_options_() { @@ -59,8 +59,8 @@ class LVGLSelect : public select::Select, public Component { const auto &opts = this->widget_->get_options(); FixedVector opt_ptrs; opt_ptrs.init(opts.size()); - for (size_t i = 0; i < opts.size(); i++) { - opt_ptrs[i] = opts[i].c_str(); + for (const auto &opt : opts) { + opt_ptrs.push_back(opt.c_str()); } this->traits.set_options(opt_ptrs); } diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 9955b530aa8..035320b6ac3 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -1,7 +1,10 @@ import sys from esphome import automation, codegen as cg +from esphome.automation import register_action +from esphome.config_validation import Schema from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE +from esphome.core import EsphomeError from esphome.cpp_generator import MockObj, MockObjClass from esphome.cpp_types import esphome_ns @@ -123,28 +126,47 @@ class WidgetType: schema=None, modify_schema=None, lv_name=None, + is_mock: bool = False, ): """ :param name: The widget name, e.g. "bar" :param w_type: The C type of the widget :param parts: What parts this widget supports :param schema: The config schema for defining a widget - :param modify_schema: A schema to update the widget + :param modify_schema: A schema to update the widget, defaults to the same as the schema + :param lv_name: The name of the LVGL widget in the LVGL library, if different from the name + :param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget """ self.name = name self.lv_name = lv_name or name self.w_type = w_type self.parts = parts - if schema is None: - self.schema = {} - else: - self.schema = schema + if not isinstance(schema, Schema): + schema = Schema(schema or {}) + self.schema = schema if modify_schema is None: - self.modify_schema = self.schema - else: - self.modify_schema = modify_schema + modify_schema = schema + if not isinstance(modify_schema, Schema): + modify_schema = Schema(modify_schema) + self.modify_schema = modify_schema self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") + # Local import to avoid circular import + from .automation import update_to_code + from .schemas import WIDGET_TYPES, create_modify_schema + + if not is_mock: + if self.name in WIDGET_TYPES: + raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") + WIDGET_TYPES[self.name] = self + + # Register the update action automatically + register_action( + f"lvgl.{self.name}.update", + ObjUpdateAction, + create_modify_schema(self), + )(update_to_code) + @property def animated(self): return False @@ -163,7 +185,6 @@ class WidgetType: :param config: Its configuration :return: Generated code as a list of text lines """ - return [] async def obj_creator(self, parent: MockObjClass, config: dict): """ @@ -174,6 +195,13 @@ class WidgetType: """ return lv_expr.call(f"{self.lv_name}_create", parent) + def on_create(self, var: MockObj, config: dict): + """ + Called from to_code when the widget is created, to set up any initial properties + :param var: The variable representing the widget + :param config: Its configuration + """ + def get_uses(self): """ Get a list of other widgets used by this one @@ -193,6 +221,14 @@ class WidgetType: def get_scale(self, config: dict): return 1.0 + def validate(self, value): + """ + Provides an opportunity for custom validation for a given widget type + :param value: + :return: + """ + return value + class NumberType(WidgetType): def get_max(self, config: dict): diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 1f9cdde0a06..187b5828c24 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -213,17 +213,14 @@ class LvScrActType(WidgetType): """ def __init__(self): - super().__init__("lv_scr_act()", lv_obj_t, ()) + super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True) async def to_code(self, w, config: dict): return [] -lv_scr_act_spec = LvScrActType() - - def get_scr_act(lv_comp: MockObj) -> Widget: - return Widget.create(None, lv_comp.get_scr_act(), lv_scr_act_spec, {}) + return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {}) def get_widget_generator(wid): @@ -339,7 +336,10 @@ async def set_obj_properties(w: Widget, config): if layout_type == TYPE_FLEX: lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) main = literal(layout[CONF_FLEX_ALIGN_MAIN]) - cross = literal(layout[CONF_FLEX_ALIGN_CROSS]) + cross = layout[CONF_FLEX_ALIGN_CROSS] + if cross == "LV_FLEX_ALIGN_STRETCH": + cross = "LV_FLEX_ALIGN_CENTER" + cross = literal(cross) track = literal(layout[CONF_FLEX_ALIGN_TRACK]) lv_obj.set_flex_align(w.obj, main, cross, track) parts = collect_parts(config) @@ -446,9 +446,11 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent): if spec.is_compound(): var = cg.new_Pvariable(wid) lv_add(var.set_obj(creator)) + spec.on_create(var.obj, w_cnfig) else: var = lv_Pvariable(lv_obj_t, wid) lv_assign(var, creator) + spec.on_create(var, w_cnfig) w = Widget.create(wid, var, spec, w_cnfig) if theme := theme_widget_map.get(w_type): diff --git a/esphome/components/lvgl/widgets/buttonmatrix.py b/esphome/components/lvgl/widgets/buttonmatrix.py index baeb1c8e3ef..fe421aa4770 100644 --- a/esphome/components/lvgl/widgets/buttonmatrix.py +++ b/esphome/components/lvgl/widgets/buttonmatrix.py @@ -1,8 +1,9 @@ from esphome import automation import esphome.codegen as cg +from esphome.components.const import CONF_ROWS from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH +from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH from esphome.cpp_generator import MockObj from ..automation import action_to_code diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index f0a9cd35bac..ead352aa77d 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -159,18 +159,15 @@ async def canvas_set_pixel(config, action_id, template_arg, args): ) -DRAW_SCHEMA = cv.Schema( - { - cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), - cv.Required(CONF_X): pixels, - cv.Required(CONF_Y): pixels, - } -) -DRAW_OPA_SCHEMA = DRAW_SCHEMA.extend( - { - cv.Optional(CONF_OPA): opacity, - } -) +DRAW_SCHEMA = { + cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), + cv.Required(CONF_X): pixels, + cv.Required(CONF_Y): pixels, +} +DRAW_OPA_SCHEMA = { + **DRAW_SCHEMA, + cv.Optional(CONF_OPA): opacity, +} async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg, args): @@ -224,12 +221,14 @@ RECT_PROPS = { @automation.register_action( "lvgl.canvas.draw_rectangle", ObjUpdateAction, - DRAW_SCHEMA.extend( + cv.Schema( { + **DRAW_OPA_SCHEMA, cv.Required(CONF_WIDTH): cv.templatable(cv.int_), cv.Required(CONF_HEIGHT): cv.templatable(cv.int_), - }, - ).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}), + **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, + } + ), ) async def canvas_draw_rect(config, action_id, template_arg, args): width = await pixels.process(config[CONF_WIDTH]) @@ -261,13 +260,14 @@ TEXT_PROPS = { @automation.register_action( "lvgl.canvas.draw_text", ObjUpdateAction, - TEXT_SCHEMA.extend(DRAW_OPA_SCHEMA) - .extend( + cv.Schema( { + **TEXT_SCHEMA, + **DRAW_OPA_SCHEMA, cv.Required(CONF_MAX_WIDTH): cv.templatable(cv.int_), + **{cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}, }, - ) - .extend({cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}), + ), ) async def canvas_draw_text(config, action_id, template_arg, args): text = await lv_text.process(config[CONF_TEXT]) @@ -293,13 +293,15 @@ IMG_PROPS = { @automation.register_action( "lvgl.canvas.draw_image", ObjUpdateAction, - DRAW_OPA_SCHEMA.extend( + cv.Schema( { + **DRAW_OPA_SCHEMA, cv.Required(CONF_SRC): lv_image, cv.Optional(CONF_PIVOT_X, default=0): pixels, cv.Optional(CONF_PIVOT_Y, default=0): pixels, - }, - ).extend({cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}), + **{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}, + } + ), ) async def canvas_draw_image(config, action_id, template_arg, args): src = await lv_image.process(config[CONF_SRC]) @@ -336,8 +338,9 @@ LINE_PROPS = { cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), cv.Optional(CONF_OPA): opacity, cv.Required(CONF_POINTS): cv.ensure_list(point_schema), - }, - ).extend({cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}), + **{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}, + } + ), ) async def canvas_draw_line(config, action_id, template_arg, args): points = [ @@ -363,8 +366,9 @@ async def canvas_draw_line(config, action_id, template_arg, args): { cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t), cv.Required(CONF_POINTS): cv.ensure_list(point_schema), + **{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}, }, - ).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}), + ), ) async def canvas_draw_polygon(config, action_id, template_arg, args): points = [ @@ -395,13 +399,15 @@ ARC_PROPS = { @automation.register_action( "lvgl.canvas.draw_arc", ObjUpdateAction, - DRAW_OPA_SCHEMA.extend( + cv.Schema( { + **DRAW_OPA_SCHEMA, cv.Required(CONF_RADIUS): pixels, cv.Required(CONF_START_ANGLE): lv_angle_degrees, cv.Required(CONF_END_ANGLE): lv_angle_degrees, + **{cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}, } - ).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}), + ), ) async def canvas_draw_arc(config, action_id, template_arg, args): radius = await size.process(config[CONF_RADIUS]) diff --git a/esphome/components/lvgl/widgets/checkbox.py b/esphome/components/lvgl/widgets/checkbox.py index c344fbfe75f..ca97e2d8432 100644 --- a/esphome/components/lvgl/widgets/checkbox.py +++ b/esphome/components/lvgl/widgets/checkbox.py @@ -17,11 +17,10 @@ class CheckboxType(WidgetType): CONF_CHECKBOX, LvBoolean("lv_checkbox_t"), (CONF_MAIN, CONF_INDICATOR), - TEXT_SCHEMA.extend( - { - Optional(CONF_PAD_COLUMN): padding, - } - ), + { + **TEXT_SCHEMA, + Optional(CONF_PAD_COLUMN): padding, + }, ) async def to_code(self, w: Widget, config): diff --git a/esphome/components/lvgl/widgets/container.py b/esphome/components/lvgl/widgets/container.py new file mode 100644 index 00000000000..2ac1a3b244b --- /dev/null +++ b/esphome/components/lvgl/widgets/container.py @@ -0,0 +1,39 @@ +import esphome.config_validation as cv +from esphome.const import CONF_HEIGHT, CONF_WIDTH +from esphome.cpp_generator import MockObj + +from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR +from ..lv_validation import size +from ..lvcode import lv +from ..types import WidgetType, lv_obj_t + +CONTAINER_SCHEMA = cv.Schema( + { + cv.Optional(CONF_HEIGHT, default="100%"): size, + cv.Optional(CONF_WIDTH, default="100%"): size, + } +) + + +class ContainerType(WidgetType): + """ + A simple container widget that can hold other widgets and which defaults to a 100% size. + Made from an obj with all styles removed + """ + + def __init__(self): + super().__init__( + CONF_CONTAINER, + lv_obj_t, + (CONF_MAIN, CONF_SCROLLBAR), + schema=CONTAINER_SCHEMA, + modify_schema={}, + lv_name=CONF_OBJ, + ) + self.styles = {} + + def on_create(self, var: MockObj, config: dict): + lv.obj_remove_style_all(var) + + +container_spec = ContainerType() diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index 6b042356749..3a3a9977374 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -23,12 +23,11 @@ class LabelType(WidgetType): CONF_LABEL, LvText("lv_label_t"), (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED), - TEXT_SCHEMA.extend( - { - cv.Optional(CONF_RECOLOR): lv_bool, - cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, - } - ), + { + **TEXT_SCHEMA, + cv.Optional(CONF_RECOLOR): lv_bool, + cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, + }, ) async def to_code(self, w: Widget, config): diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 028a81b4499..ad46f67c6b6 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -14,13 +14,12 @@ CONF_QRCODE = "qrcode" CONF_DARK_COLOR = "dark_color" CONF_LIGHT_COLOR = "light_color" -QRCODE_SCHEMA = TEXT_SCHEMA.extend( - { - cv.Optional(CONF_DARK_COLOR, default="black"): lv_color, - cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, - cv.Required(CONF_SIZE): cv.int_, - } -) +QRCODE_SCHEMA = { + **TEXT_SCHEMA, + cv.Optional(CONF_DARK_COLOR, default="black"): lv_color, + cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, + cv.Required(CONF_SIZE): cv.int_, +} class QrCodeType(WidgetType): diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index 26ad149c6f9..ac23ded723f 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE -from ..automation import action_to_code, update_to_code +from ..automation import action_to_code from ..defines import ( CONF_CURSOR, CONF_DECIMAL_PLACES, @@ -171,17 +171,3 @@ async def spinbox_decrement(config, action_id, template_arg, args): lv.spinbox_decrement(w.obj) return await action_to_code(widgets, do_increment, action_id, template_arg, args) - - -@automation.register_action( - "lvgl.spinbox.update", - ObjUpdateAction, - cv.Schema( - { - cv.Required(CONF_ID): cv.use_id(lv_spinbox_t), - cv.Required(CONF_VALUE): lv_float, - } - ), -) -async def spinbox_update_to_code(config, action_id, template_arg, args): - return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/textarea.py b/esphome/components/lvgl/widgets/textarea.py index 23d50b3894b..e5ab8846859 100644 --- a/esphome/components/lvgl/widgets/textarea.py +++ b/esphome/components/lvgl/widgets/textarea.py @@ -21,15 +21,14 @@ CONF_TEXTAREA = "textarea" lv_textarea_t = LvText("lv_textarea_t") -TEXTAREA_SCHEMA = TEXT_SCHEMA.extend( - { - cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, - cv.Optional(CONF_ACCEPTED_CHARS): lv_text, - cv.Optional(CONF_ONE_LINE): lv_bool, - cv.Optional(CONF_PASSWORD_MODE): lv_bool, - cv.Optional(CONF_MAX_LENGTH): lv_int, - } -) +TEXTAREA_SCHEMA = { + **TEXT_SCHEMA, + cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text, + cv.Optional(CONF_ACCEPTED_CHARS): lv_text, + cv.Optional(CONF_ONE_LINE): lv_bool, + cv.Optional(CONF_PASSWORD_MODE): lv_bool, + cv.Optional(CONF_MAX_LENGTH): lv_int, +} class TextareaType(WidgetType): diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 2e123323a08..868b1492115 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -1,8 +1,9 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import key_provider +from esphome.components.const import CONF_ROWS import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID +from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID CODEOWNERS = ["@ssieb"] diff --git a/esphome/components/max17043/automation.h b/esphome/components/max17043/automation.h index 44729d119b1..ac201a73096 100644 --- a/esphome/components/max17043/automation.h +++ b/esphome/components/max17043/automation.h @@ -10,7 +10,7 @@ template class SleepAction : public Action { public: explicit SleepAction(MAX17043Component *max17043) : max17043_(max17043) {} - void play(Ts... x) override { this->max17043_->sleep_mode(); } + void play(const Ts &...x) override { this->max17043_->sleep_mode(); } protected: MAX17043Component *max17043_; diff --git a/esphome/components/max6956/automation.h b/esphome/components/max6956/automation.h index c0b491dc7f9..ca2c3e3ce4d 100644 --- a/esphome/components/max6956/automation.h +++ b/esphome/components/max6956/automation.h @@ -13,7 +13,7 @@ template class SetCurrentGlobalAction : public Action { TEMPLATABLE_VALUE(uint8_t, brightness_global) - void play(Ts... x) override { + void play(const Ts &...x) override { this->max6956_->set_brightness_global(this->brightness_global_.value(x...)); this->max6956_->write_brightness_global(); } @@ -28,7 +28,7 @@ template class SetCurrentModeAction : public Action { TEMPLATABLE_VALUE(max6956::MAX6956CURRENTMODE, brightness_mode) - void play(Ts... x) override { + void play(const Ts &...x) override { this->max6956_->set_brightness_mode(this->brightness_mode_.value(x...)); this->max6956_->write_brightness_mode(); } diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 270edf3282f..58d871d54c7 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -4,13 +4,14 @@ #include "esphome/core/time.h" #include "esphome/components/spi/spi.h" +#include "esphome/components/display/display.h" namespace esphome { namespace max7219 { class MAX7219Component; -using max7219_writer_t = std::function; +using max7219_writer_t = display::DisplayWriter; class MAX7219Component : public PollingComponent, public spi::SPIDevice writer_{}; + max7219_writer_t writer_{}; }; } // namespace max7219 diff --git a/esphome/components/max7219digit/automation.h b/esphome/components/max7219digit/automation.h index 02acebb1091..be8245d14d3 100644 --- a/esphome/components/max7219digit/automation.h +++ b/esphome/components/max7219digit/automation.h @@ -12,7 +12,7 @@ template class DisplayInvertAction : public Action, publi public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { bool state = this->state_.value(x...); this->parent_->invert_on_off(state); } @@ -22,7 +22,7 @@ template class DisplayVisibilityAction : public Action, p public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { bool state = this->state_.value(x...); this->parent_->turn_on_off(state); } @@ -32,7 +32,7 @@ template class DisplayReverseAction : public Action, publ public: TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { bool state = this->state_.value(x...); this->parent_->set_reverse(state); } @@ -42,7 +42,7 @@ template class DisplayIntensityAction : public Action, pu public: TEMPLATABLE_VALUE(uint8_t, state) - void play(Ts... x) override { + void play(const Ts &...x) override { uint8_t state = this->state_.value(x...); this->parent_->set_intensity(state); } diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 6df3c4d7c82..cdceafad501 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -271,7 +271,11 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } } } else if (this->orientation_ == 1) { - b = pixels[col]; + if (this->flip_x_) { + b = pixels[7 - col]; + } else { + b = pixels[col]; + } } else if (this->orientation_ == 2) { for (uint8_t i = 0; i < 8; i++) { if (this->flip_x_) { @@ -282,7 +286,11 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } } else { for (uint8_t i = 0; i < 8; i++) { - b |= ((pixels[7 - col] >> i) & 1) << (7 - i); + if (this->flip_x_) { + b |= ((pixels[col] >> i) & 1) << (7 - i); + } else { + b |= ((pixels[7 - col] >> i) & 1) << (7 - i); + } } } // send this byte to display at selected chip diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index ead80338035..af419b9b382 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -23,7 +23,7 @@ enum ScrollMode { class MAX7219Component; -using max7219_writer_t = std::function; +using max7219_writer_t = display::DisplayWriter; class MAX7219Component : public display::DisplayBuffer, public spi::SPIDevice writer_local_{}; + max7219_writer_t writer_local_{}; }; } // namespace max7219digit diff --git a/esphome/components/mcp3221/__init__.py b/esphome/components/mcp3221/__init__.py new file mode 100644 index 00000000000..677bb78c358 --- /dev/null +++ b/esphome/components/mcp3221/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@philippderdiedas"] diff --git a/esphome/components/mcp3221/mcp3221_sensor.cpp b/esphome/components/mcp3221/mcp3221_sensor.cpp new file mode 100644 index 00000000000..c04b1c0b935 --- /dev/null +++ b/esphome/components/mcp3221/mcp3221_sensor.cpp @@ -0,0 +1,31 @@ +#include "mcp3221_sensor.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3221 { + +static const char *const TAG = "mcp3221"; + +float MCP3221Sensor::sample() { + uint8_t data[2]; + if (this->read(data, 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Read failed"); + this->status_set_warning(); + return NAN; + } + this->status_clear_warning(); + + uint16_t value = encode_uint16(data[0], data[1]); + float voltage = value * this->reference_voltage_ / 4096.0f; + + return voltage; +} + +void MCP3221Sensor::update() { + float v = this->sample(); + this->publish_state(v); +} + +} // namespace mcp3221 +} // namespace esphome diff --git a/esphome/components/mcp3221/mcp3221_sensor.h b/esphome/components/mcp3221/mcp3221_sensor.h new file mode 100644 index 00000000000..c83caccabfe --- /dev/null +++ b/esphome/components/mcp3221/mcp3221_sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" + +#include + +namespace esphome { +namespace mcp3221 { + +class MCP3221Sensor : public sensor::Sensor, + public PollingComponent, + public voltage_sampler::VoltageSampler, + public i2c::I2CDevice { + public: + void set_reference_voltage(float reference_voltage) { this->reference_voltage_ = reference_voltage; } + void update() override; + float sample() override; + + protected: + float reference_voltage_; +}; + +} // namespace mcp3221 +} // namespace esphome diff --git a/esphome/components/mcp3221/sensor.py b/esphome/components/mcp3221/sensor.py new file mode 100644 index 00000000000..993876c2c83 --- /dev/null +++ b/esphome/components/mcp3221/sensor.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +from esphome.components import i2c, sensor, voltage_sampler +import esphome.config_validation as cv +from esphome.const import ( + CONF_REFERENCE_VOLTAGE, + DEVICE_CLASS_VOLTAGE, + ICON_SCALE, + STATE_CLASS_MEASUREMENT, + UNIT_VOLT, +) + +AUTO_LOAD = ["voltage_sampler"] +DEPENDENCIES = ["i2c"] + + +mcp3221_ns = cg.esphome_ns.namespace("mcp3221") +MCP3221Sensor = mcp3221_ns.class_( + "MCP3221Sensor", + sensor.Sensor, + voltage_sampler.VoltageSampler, + cg.PollingComponent, + i2c.I2CDevice, +) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + MCP3221Sensor, + icon=ICON_SCALE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + device_class=DEVICE_CLASS_VOLTAGE, + unit_of_measurement=UNIT_VOLT, + ) + .extend( + { + cv.Optional(CONF_REFERENCE_VOLTAGE, default="3.3V"): cv.voltage, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + cg.add(var.set_reference_voltage(config[CONF_REFERENCE_VOLTAGE])) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index d476136554d..2c3150ff5dd 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -37,8 +37,6 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); void MDNSComponent::compile_records_(StaticVector &services) { - this->hostname_ = App.get_name(); - // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES // in mdns/__init__.py. If you add a new service here, update both locations. @@ -179,7 +177,7 @@ void MDNSComponent::dump_config() { ESP_LOGCONFIG(TAG, "mDNS:\n" " Hostname: %s", - this->hostname_.c_str()); + App.get_name().c_str()); #ifdef USE_MDNS_STORE_SERVICES ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 35371fd7391..f4237d5a690 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -76,7 +76,6 @@ class MDNSComponent : public Component { #ifdef USE_MDNS_STORE_SERVICES StaticVector services_{}; #endif - std::string hostname_; void compile_records_(StaticVector &services); }; diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index c02bfcbadb8..ecdc926cc96 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -2,6 +2,7 @@ #if defined(USE_ESP32) && defined(USE_MDNS) #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -27,8 +28,9 @@ void MDNSComponent::setup() { return; } - mdns_hostname_set(this->hostname_.c_str()); - mdns_instance_name_set(this->hostname_.c_str()); + const char *hostname = App.get_name().c_str(); + mdns_hostname_set(hostname); + mdns_instance_name_set(hostname); for (const auto &service : services) { auto txt_records = std::make_unique(service.txt_records.size()); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 25a3defa7ba..9bbb4060700 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -4,6 +4,7 @@ #include #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index a3e317a2bfa..fb2088f7194 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 791fa3934d3..a9f5349f14b 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 3af5959f321..50e7693cb51 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -11,7 +11,7 @@ template class MediaPlayerCommandAction : public Action, public Parented { public: TEMPLATABLE_VALUE(bool, announcement); - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->make_call().set_command(Command).set_announcement(this->announcement_.value(x...)).perform(); } }; @@ -36,7 +36,7 @@ using TurnOffAction = MediaPlayerCommandAction class PlayMediaAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, media_url) TEMPLATABLE_VALUE(bool, announcement) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->make_call() .set_media_url(this->media_url_.value(x...)) .set_announcement(this->announcement_.value(x...)) @@ -46,7 +46,7 @@ template class PlayMediaAction : public Action, public Pa template class VolumeSetAction : public Action, public Parented { TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } + void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } }; class StateTrigger : public Trigger<> { @@ -75,32 +75,34 @@ using OffTrigger = MediaPlayerStateTrigger class IsIdleCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_IDLE; } }; template class IsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PLAYING; } }; template class IsPausedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_PAUSED; } }; template class IsAnnouncingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; } + bool check(const Ts &...x) override { + return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ANNOUNCING; + } }; template class IsOnCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ON; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_ON; } }; template class IsOffCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } + bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; } }; } // namespace media_player diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 3f274bf73b3..b46ec39d302 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -1,5 +1,6 @@ #include "media_player.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -148,7 +149,12 @@ void MediaPlayer::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -void MediaPlayer::publish_state() { this->state_callback_.call(); } +void MediaPlayer::publish_state() { + this->state_callback_.call(); +#if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_media_player_update(this); +#endif +} } // namespace media_player } // namespace esphome diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index ec38f2cd2fc..be36886d622 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -40,7 +40,7 @@ template class MHZ19CalibrateZeroAction : public Action { public: MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->calibrate_zero(); } + void play(const Ts &...x) override { this->mhz19_->calibrate_zero(); } protected: MHZ19Component *mhz19_; @@ -50,7 +50,7 @@ template class MHZ19ABCEnableAction : public Action { public: MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->abc_enable(); } + void play(const Ts &...x) override { this->mhz19_->abc_enable(); } protected: MHZ19Component *mhz19_; @@ -60,7 +60,7 @@ template class MHZ19ABCDisableAction : public Action { public: MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->abc_disable(); } + void play(const Ts &...x) override { this->mhz19_->abc_disable(); } protected: MHZ19Component *mhz19_; diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index 8cd71153681..575fb97799b 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 +from esphome.components import esp32, microphone, socket import esphome.config_validation as cv from esphome.const import ( CONF_FILE, @@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kahrendt", "@jesserockz"] DEPENDENCIES = ["microphone"] +AUTO_LOAD = ["socket"] DOMAIN = "micro_wake_word" @@ -443,6 +444,10 @@ 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/automation.h b/esphome/components/micro_wake_word/automation.h index f10a4ed3470..e1795a7e644 100644 --- a/esphome/components/micro_wake_word/automation.h +++ b/esphome/components/micro_wake_word/automation.h @@ -9,23 +9,23 @@ namespace micro_wake_word { template class StartAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class IsRunningCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } }; template class EnableModelAction : public Action { public: explicit EnableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - void play(Ts... x) override { this->wake_word_model_->enable(); } + void play(const Ts &...x) override { this->wake_word_model_->enable(); } protected: WakeWordModel *wake_word_model_; @@ -34,7 +34,7 @@ template class EnableModelAction : public Action { template class DisableModelAction : public Action { public: explicit DisableModelAction(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - void play(Ts... x) override { this->wake_word_model_->disable(); } + void play(const Ts &...x) override { this->wake_word_model_->disable(); } protected: WakeWordModel *wake_word_model_; @@ -43,7 +43,7 @@ template class DisableModelAction : public Action { template class ModelIsEnabledCondition : public Condition { public: explicit ModelIsEnabledCondition(WakeWordModel *wake_word_model) : wake_word_model_(wake_word_model) {} - bool check(Ts... x) override { return this->wake_word_model_->is_enabled(); } + bool check(const Ts &...x) override { return this->wake_word_model_->is_enabled(); } protected: WakeWordModel *wake_word_model_; diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 6fca48a5bd3..a0547b158ef 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -2,6 +2,7 @@ #ifdef USE_ESP_IDF +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -426,6 +427,12 @@ void MicroWakeWord::process_probabilities_() { if (vad_state.detected) { #endif 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 } else { diff --git a/esphome/components/microphone/automation.h b/esphome/components/microphone/automation.h index 5745909c46f..a6c4bdae663 100644 --- a/esphome/components/microphone/automation.h +++ b/esphome/components/microphone/automation.h @@ -9,18 +9,18 @@ namespace esphome { namespace microphone { template class CaptureAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopCaptureAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class MuteAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_mute_state(true); } + void play(const Ts &...x) override { this->parent_->set_mute_state(true); } }; template class UnmuteAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_mute_state(false); } + void play(const Ts &...x) override { this->parent_->set_mute_state(false); } }; class DataTrigger : public Trigger &> { @@ -32,12 +32,12 @@ class DataTrigger : public Trigger &> { template class IsCapturingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } }; template class IsMutedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->get_mute_state(); } + bool check(const Ts &...x) override { return this->parent_->get_mute_state(); } }; } // namespace microphone diff --git a/esphome/components/midea/ac_adapter.cpp b/esphome/components/midea/ac_adapter.cpp index dca4038f04f..d903db4a1b8 100644 --- a/esphome/components/midea/ac_adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -117,8 +117,8 @@ const char *Converters::to_custom_climate_fan_mode(MideaFanMode mode) { } } -MideaFanMode Converters::to_midea_fan_mode(const std::string &mode) { - if (mode == Constants::SILENT) +MideaFanMode Converters::to_midea_fan_mode(const char *mode) { + if (strcmp(mode, Constants::SILENT) == 0) return MideaFanMode::FAN_SILENT; return MideaFanMode::FAN_TURBO; } @@ -153,7 +153,7 @@ bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == M const char *Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } -MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } +MideaPreset Converters::to_midea_preset(const char *preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities) { if (capabilities.supportAutoMode()) diff --git a/esphome/components/midea/ac_adapter.h b/esphome/components/midea/ac_adapter.h index d52f4213311..b0589a37f9b 100644 --- a/esphome/components/midea/ac_adapter.h +++ b/esphome/components/midea/ac_adapter.h @@ -32,12 +32,12 @@ class Converters { static MideaSwingMode to_midea_swing_mode(ClimateSwingMode mode); static ClimateSwingMode to_climate_swing_mode(MideaSwingMode mode); static MideaPreset to_midea_preset(ClimatePreset preset); - static MideaPreset to_midea_preset(const std::string &preset); + static MideaPreset to_midea_preset(const char *preset); static bool is_custom_midea_preset(MideaPreset preset); static ClimatePreset to_climate_preset(MideaPreset preset); static const char *to_custom_climate_preset(MideaPreset preset); static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode); - static MideaFanMode to_midea_fan_mode(const std::string &fan_mode); + static MideaFanMode to_midea_fan_mode(const char *fan_mode); static bool is_custom_midea_fan_mode(MideaFanMode fan_mode); static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode); static const char *to_custom_climate_fan_mode(MideaFanMode fan_mode); diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h index e6fffa2511c..760737be87e 100644 --- a/esphome/components/midea/ac_automations.h +++ b/esphome/components/midea/ac_automations.h @@ -22,7 +22,7 @@ template class FollowMeAction : public MideaActionBase { TEMPLATABLE_VALUE(bool, use_fahrenheit) TEMPLATABLE_VALUE(bool, beeper) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->do_follow_me(this->temperature_.value(x...), this->use_fahrenheit_.value(x...), this->beeper_.value(x...)); } @@ -30,37 +30,37 @@ template class FollowMeAction : public MideaActionBase { template class SwingStepAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_swing_step(); } + void play(const Ts &...x) override { this->parent_->do_swing_step(); } }; template class DisplayToggleAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_display_toggle(); } + void play(const Ts &...x) override { this->parent_->do_display_toggle(); } }; template class BeeperOnAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_beeper_on(); } + void play(const Ts &...x) override { this->parent_->do_beeper_on(); } }; template class BeeperOffAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_beeper_off(); } + void play(const Ts &...x) override { this->parent_->do_beeper_off(); } }; template class PowerOnAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_power_on(); } + void play(const Ts &...x) override { this->parent_->do_power_on(); } }; template class PowerOffAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_power_off(); } + void play(const Ts &...x) override { this->parent_->do_power_off(); } }; template class PowerToggleAction : public MideaActionBase { public: - void play(Ts... x) override { this->parent_->do_power_toggle(); } + void play(const Ts &...x) override { this->parent_->do_power_toggle(); } }; } // namespace ac diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 97eacb936ca..a6a8d525499 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -64,13 +64,13 @@ void AirConditioner::control(const ClimateCall &call) { ctrl.mode = Converters::to_midea_mode(call.get_mode().value()); if (call.get_preset().has_value()) { ctrl.preset = Converters::to_midea_preset(call.get_preset().value()); - } else if (call.get_custom_preset().has_value()) { - ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().value()); + } else if (call.has_custom_preset()) { + ctrl.preset = Converters::to_midea_preset(call.get_custom_preset()); } if (call.get_fan_mode().has_value()) { ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value()); - } else if (call.get_custom_fan_mode().has_value()) { - ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().value()); + } else if (call.has_custom_fan_mode()) { + ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode()); } this->base_.control(ctrl); } diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 93d1750cd60..4dbc81caa2b 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -218,6 +218,21 @@ def map_sequence(value): return tuple(value) +def flatten_sequence(sequence: tuple | list): + """ + Flatten an init sequence into a single list of bytes. + :param sequence: The list of tuples + :return: a list of bytes + """ + return sum( + tuple( + (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] + for x in sequence + ), + (), + ) + + def delay(ms): return DELAY_FLAG, ms @@ -456,13 +471,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 sum( - tuple( - (x[1], 0xFF) if x[0] == DELAY_FLAG else (x[0], len(x) - 1) + x[1:] - for x in sequence - ), - (), - ), madctl + return flatten_sequence(sequence), madctl def requires_buffer(config) -> bool: diff --git a/esphome/components/mixer/speaker/automation.h b/esphome/components/mixer/speaker/automation.h index b688fa2c1ec..22349366289 100644 --- a/esphome/components/mixer/speaker/automation.h +++ b/esphome/components/mixer/speaker/automation.h @@ -9,7 +9,7 @@ namespace mixer_speaker { template class DuckingApplyAction : public Action, public Parented { TEMPLATABLE_VALUE(uint8_t, decibel_reduction) TEMPLATABLE_VALUE(uint32_t, duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...)); } }; diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 28f3326c475..1c23783ce37 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -3,6 +3,7 @@ import binascii 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, @@ -20,7 +21,6 @@ from .const import ( CONF_BYTE_OFFSET, CONF_COMMAND_THROTTLE, CONF_CUSTOM_COMMAND, - CONF_ENABLED, CONF_FORCE_NEW_RANGE, CONF_MAX_CMD_RETRIES, CONF_MODBUS_CONTROLLER_ID, diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index ee0b5fc633f..c689d845765 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -2,7 +2,6 @@ CONF_ALLOW_DUPLICATE_COMMANDS = "allow_duplicate_commands" CONF_BITMASK = "bitmask" CONF_BYTE_OFFSET = "byte_offset" CONF_COMMAND_THROTTLE = "command_throttle" -CONF_ENABLED = "enabled" CONF_OFFLINE_SKIP_UPDATES = "offline_skip_updates" CONF_CUSTOM_COMMAND = "custom_command" CONF_FORCE_NEW_RANGE = "force_new_range" diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 48bf2835f2c..853f4215c35 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -28,8 +28,9 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { if (map_it != this->mapping_.cend()) { size_t idx = std::distance(this->mapping_.cbegin(), map_it); - new_state = std::string(this->option_at(idx)); - ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); + ESP_LOGV(TAG, "Found option %s for value %lld", this->option_at(idx), value); + this->publish_state(idx); + return; } else { ESP_LOGE(TAG, "No option found for mapping %lld", value); } @@ -40,19 +41,16 @@ void ModbusSelect::parse_and_publish(const std::vector &data) { } } -void ModbusSelect::control(const std::string &value) { - auto idx = this->index_of(value); - if (!idx.has_value()) { - ESP_LOGW(TAG, "Invalid option '%s'", value.c_str()); - return; - } - optional mapval = this->mapping_[idx.value()]; - ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); +void ModbusSelect::control(size_t index) { + optional mapval = this->mapping_[index]; + const char *option = this->option_at(index); + ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, option); std::vector data; if (this->write_transform_func_.has_value()) { - auto val = (*this->write_transform_func_)(this, value, *mapval, data); + // Transform func requires string parameter for backward compatibility + auto val = (*this->write_transform_func_)(this, std::string(option), *mapval, data); if (val.has_value()) { mapval = *val; ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval); @@ -85,7 +83,7 @@ void ModbusSelect::control(const std::string &value) { this->parent_->queue_command(write_cmd); if (this->optimistic_) - this->publish_state(value); + this->publish_state(index); } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index e6b98aead28..fde441f2bcf 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -38,7 +38,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem void dump_config() override; void parse_and_publish(const std::vector &data) override; - void control(const std::string &value) override; + void control(size_t index) override; protected: std::vector mapping_{}; diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.cpp b/esphome/components/mopeka_std_check/mopeka_std_check.cpp index 6685a23c411..0d8340f95fb 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.cpp +++ b/esphome/components/mopeka_std_check/mopeka_std_check.cpp @@ -72,7 +72,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF; if (static_cast(hardware_id) != STANDARD && static_cast(hardware_id) != XL && - static_cast(hardware_id) != ETRAILER) { + static_cast(hardware_id) != ETRAILER && static_cast(hardware_id) != STANDARD_ALT) { ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id); return false; } diff --git a/esphome/components/mopeka_std_check/mopeka_std_check.h b/esphome/components/mopeka_std_check/mopeka_std_check.h index b92445df34b..897b5414ed0 100644 --- a/esphome/components/mopeka_std_check/mopeka_std_check.h +++ b/esphome/components/mopeka_std_check/mopeka_std_check.h @@ -15,6 +15,7 @@ namespace mopeka_std_check { enum SensorType { STANDARD = 0x02, XL = 0x03, + STANDARD_ALT = 0x44, ETRAILER = 0x46, }; diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 641c70a367c..1fc0c30db1c 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -3,7 +3,7 @@ import re from esphome import automation from esphome.automation import Condition import esphome.codegen as cg -from esphome.components import logger +from esphome.components import logger, socket from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv @@ -66,6 +66,9 @@ 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"] @@ -213,8 +216,6 @@ def validate_fingerprint(value): def _consume_mqtt_sockets(config: ConfigType) -> ConfigType: """Register socket needs for MQTT component.""" - from esphome.components import socket - # MQTT needs 1 socket for the broker connection socket.consume_sockets(1, "mqtt")(config) return config @@ -341,6 +342,11 @@ 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() + cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 94460c31a75..dd3df5f8aa6 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -36,7 +36,7 @@ void MQTTAlarmControlPanelComponent::setup() { } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { call.triggered(); } else { - ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str()); } call.perform(); }); diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 623206a0cd7..dcc51ed60ee 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -190,6 +190,11 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b if (instance) { auto event = *static_cast(event_data); instance->mqtt_events_.emplace(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) + App.wake_loop_threadsafe(); +#endif } } diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 2ce4928574f..479cee205ad 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,9 +30,12 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor } void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->binary_sensor_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->binary_sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index b3435edf381..f8eb0eab2d7 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -20,7 +20,7 @@ void MQTTButtonComponent::setup() { if (payload == "PRESS") { this->button_->press(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); @@ -33,8 +33,9 @@ void MQTTButtonComponent::dump_config() { void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - if (!this->button_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); + const auto device_class = this->button_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 325ca56f4b6..79383ee857b 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -389,7 +389,7 @@ template class MQTTPublishAction : public Action { TEMPLATABLE_VALUE(uint8_t, qos) TEMPLATABLE_VALUE(bool, retain) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->publish(this->topic_.value(x...), this->payload_.value(x...), this->qos_.value(x...), this->retain_.value(x...)); } @@ -407,7 +407,7 @@ template class MQTTPublishJsonAction : public Action { void set_payload(std::function payload) { this->payload_ = payload; } - void play(Ts... x) override { + void play(const Ts &...x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); auto qos = this->qos_.value(x...); @@ -424,7 +424,7 @@ template class MQTTPublishJsonAction : public Action { template class MQTTConnectedCondition : public Condition { public: MQTTConnectedCondition(MQTTClientComponent *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->is_connected(); } + bool check(const Ts &...x) override { return this->parent_->is_connected(); } protected: MQTTClientComponent *parent_; @@ -434,7 +434,7 @@ template class MQTTEnableAction : public Action { public: MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->enable(); } + void play(const Ts &...x) override { this->parent_->enable(); } protected: MQTTClientComponent *parent_; @@ -444,7 +444,7 @@ template class MQTTDisableAction : public Action { public: MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->disable(); } + void play(const Ts &...x) override { this->parent_->disable(); } protected: MQTTClientComponent *parent_; diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index a6f4e0a2018..aee2b389421 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -357,8 +357,8 @@ bool MQTTClimateComponent::publish_state_() { payload = "unknown"; } } - if (this->device_->custom_preset.has_value()) - payload = this->device_->custom_preset.value(); + if (this->device_->has_custom_preset()) + payload = this->device_->get_custom_preset(); if (!this->publish(this->get_preset_state_topic(), payload)) success = false; } @@ -429,8 +429,8 @@ bool MQTTClimateComponent::publish_state_() { payload = "unknown"; } } - if (this->device_->custom_fan_mode.has_value()) - payload = this->device_->custom_fan_mode.value(); + if (this->device_->has_custom_fan_mode()) + payload = this->device_->get_custom_fan_mode(); if (!this->publish(this->get_fan_mode_state_topic(), payload)) success = false; } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index eb6114008a6..1cd818964eb 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -64,11 +64,11 @@ bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (discovery_info.clean) { - ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str()); return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true); } - ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str()); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return global_mqtt_client->publish_json( @@ -85,12 +85,16 @@ bool MQTTComponent::send_discovery_() { } // Fields from EntityBase - root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : ""; + root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : ""; - if (this->is_disabled_by_default()) + if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; - if (!this->get_icon().empty()) - root[MQTT_ICON] = this->get_icon(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto icon_ref = this->get_icon_ref_(); + if (!icon_ref.empty()) { + root[MQTT_ICON] = icon_ref; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) const auto entity_category = this->get_entity()->get_entity_category(); switch (entity_category) { @@ -122,7 +126,7 @@ bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; - sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name())); + sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_())); friendly_name_hash[8] = 0; // ensure the hash-string ends with null root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; } else { @@ -184,7 +188,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return str_sanitize(str_snake_case(this->friendly_name())); + return str_sanitize(str_snake_case(this->friendly_name_())); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { @@ -268,9 +272,9 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } // Pull these properties from EntityBase if not overridden -std::string MQTTComponent::friendly_name() const { return this->get_entity()->get_name(); } -std::string MQTTComponent::get_icon() const { return this->get_entity()->get_icon(); } -bool MQTTComponent::is_disabled_by_default() const { return this->get_entity()->is_disabled_by_default(); } +std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } +StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); } +bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::is_internal() { if (this->has_custom_state_topic_) { // If the custom state_topic is null, return true as it is internal and should not publish diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 851fdd842c1..2f8dfcf64ed 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -165,13 +165,13 @@ class MQTTComponent : public Component { virtual const EntityBase *get_entity() const = 0; /// Get the friendly name of this MQTT component. - virtual std::string friendly_name() const; + std::string friendly_name_() const; - /// Get the icon field of this component - virtual std::string get_icon() const; + /// Get the icon field of this component as StringRef + StringRef get_icon_ref_() const; /// Get whether the underlying Entity is disabled by default - virtual bool is_disabled_by_default() const; + bool is_disabled_by_default_() const; /// Get the MQTT topic that new states will be shared to. std::string get_state_topic_() const; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 6fb61ee4693..b63aa66d295 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -67,9 +67,12 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->cover_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->cover_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index f972d545c6a..fd095ea041b 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -21,8 +21,12 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); - if (!this->event_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->event_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->event_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } @@ -34,8 +38,8 @@ void MQTTEventComponent::setup() { void MQTTEventComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT Event '%s': ", this->event_->get_name().c_str()); ESP_LOGCONFIG(TAG, "Event Types: "); - for (const auto &event_type : this->event_->get_event_types()) { - ESP_LOGCONFIG(TAG, "- %s", event_type.c_str()); + for (const char *event_type : this->event_->get_event_types()) { + ESP_LOGCONFIG(TAG, "- %s", event_type); } LOG_MQTT_COMPONENT(true, true); } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 70e1ae3b4ad..2aefc3a4db5 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -24,15 +24,15 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str()); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name_().c_str()); this->state_->turn_on().perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name_().c_str()); this->state_->turn_off().perform(); break; case PARSE_TOGGLE: - ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name_().c_str()); this->state_->toggle().perform(); break; case PARSE_NONE: @@ -48,11 +48,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "forward", "reverse"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform(); break; case PARSE_TOGGLE: @@ -75,11 +75,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(true).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(false).perform(); break; case PARSE_TOGGLE: diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 04126249834..0e15377ba45 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -24,7 +24,7 @@ void MQTTLockComponent::setup() { } else if (strcasecmp(payload.c_str(), "OPEN") == 0) { this->lock_->open(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index a44632ff308..f419eac130f 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -44,8 +44,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MIN] = traits.get_min_value(); root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); - if (!this->number_->traits.get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->number_->traits.get_unit_of_measurement(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) { + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } switch (this->number_->traits.get_mode()) { case NUMBER_MODE_AUTO: break; @@ -56,8 +59,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = "slider"; break; } - if (!this->number_->traits.get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->number_->traits.get_device_class(); + const auto device_class = this->number_->traits.get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = true; } diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 9e61f6ef3b6..010ac3013ef 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,13 +44,17 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } - if (!this->sensor_->get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); + const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) { + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->get_expire_after() > 0) root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000; diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index 8b1323bdb24..b3a35420b92 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -29,7 +29,7 @@ void MQTTSwitchComponent::setup() { break; case PARSE_NONE: default: - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); break; } diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 42260ed2a8c..e6e7cf04e8a 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -15,10 +15,12 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index 5d4807c7f37..20f3a69a9e0 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -20,7 +20,7 @@ void MQTTUpdateComponent::setup() { if (payload == "INSTALL") { this->update_->perform(); } else { - ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 551398cf42a..ae606707482 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -49,10 +49,12 @@ void MQTTValveComponent::dump_config() { } } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->valve_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->valve_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) auto traits = this->valve_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/nau7802/nau7802.h b/esphome/components/nau7802/nau7802.h index 17e426ccc6c..05452851caf 100644 --- a/esphome/components/nau7802/nau7802.h +++ b/esphome/components/nau7802/nau7802.h @@ -101,18 +101,18 @@ class NAU7802Sensor : public sensor::Sensor, public PollingComponent, public i2c template class NAU7802CalbrateExternalOffsetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->calibrate_external_offset(); } + void play(const Ts &...x) override { this->parent_->calibrate_external_offset(); } }; template class NAU7802CalbrateInternalOffsetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->calibrate_internal_offset(); } + void play(const Ts &...x) override { this->parent_->calibrate_internal_offset(); } }; template class NAU7802CalbrateGainAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->calibrate_gain(); } + void play(const Ts &...x) override { this->parent_->calibrate_gain(); } }; } // namespace nau7802 diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 1d62b661caa..d7a51fb0c6c 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,7 +1,9 @@ import ipaddress +import logging import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.psram import is_guaranteed as psram_is_guaranteed import esphome.config_validation as cv from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT from esphome.core import CORE, CoroPriority, coroutine_with_priority @@ -9,6 +11,13 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] +_LOGGER = logging.getLogger(__name__) + +# High performance networking tracking infrastructure +# Components can request high performance networking and this configures lwip and WiFi settings +KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking" +CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" + network_ns = cg.esphome_ns.namespace("network") IPAddress = network_ns.class_("IPAddress") @@ -47,6 +56,55 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj: return IPAddress(str(ip)) +def require_high_performance_networking() -> None: + """Request high performance networking for network and WiFi. + + Call this from components that need optimized network performance for streaming + or high-throughput data transfer. This enables high performance mode which + configures both lwip TCP settings and WiFi driver settings for improved + network performance. + + Settings applied (ESP-IDF only): + - lwip: Larger TCP buffers, windows, and mailbox sizes + - WiFi: Increased RX/TX buffers, AMPDU aggregation, PSRAM allocation (set by wifi component) + + Configuration is PSRAM-aware: + - With PSRAM guaranteed: Aggressive settings (512 RX buffers, 512KB TCP windows) + - Without PSRAM: Conservative optimized settings (64 buffers, 65KB TCP windows) + + Example: + from esphome.components import network + + def _request_high_performance_networking(config): + network.require_high_performance_networking() + return config + + CONFIG_SCHEMA = cv.All( + ..., + _request_high_performance_networking, + ) + """ + # Only set up once (idempotent - multiple components can call this) + if not CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False): + CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True + + +def has_high_performance_networking() -> bool: + """Check if high performance networking mode is enabled. + + Returns True when high performance networking has been requested by a + component or explicitly enabled in the network configuration. This indicates + that lwip and WiFi will use optimized buffer sizes and settings. + + This function should be called during code generation (to_code phase) by + components that need to apply performance-related settings. + + Returns: + bool: True if high performance networking is enabled, False otherwise + """ + return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( @@ -71,6 +129,7 @@ CONFIG_SCHEMA = cv.Schema( ), ), cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, + cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32), } ) @@ -80,6 +139,70 @@ async def to_code(config): cg.add_define("USE_NETWORK") if CORE.using_arduino and CORE.is_esp32: cg.add_library("Networking", None) + + # Apply high performance networking settings + # Config can explicitly enable/disable, or default to component-driven behavior + enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE) + component_requested = CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) + + # Explicit config overrides component request + should_enable = ( + enable_high_perf if enable_high_perf is not None else component_requested + ) + + # Log when user explicitly disables but a component requested it + if enable_high_perf is False and component_requested: + _LOGGER.info( + "High performance networking disabled by user configuration (overriding component request)" + ) + + if CORE.is_esp32 and CORE.using_esp_idf and should_enable: + # Check if PSRAM is guaranteed (set by psram component during final validation) + psram_guaranteed = psram_is_guaranteed() + + if psram_guaranteed: + _LOGGER.info( + "Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes" + ) + # PSRAM is guaranteed - use aggressive settings + # Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true + # CONFIG_LWIP_WND_SCALE can only be enabled if CONFIG_SPIRAM_IGNORE_NOTFOUND isn't set + # Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + + # Enable window scaling for much larger TCP windows + add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3) + + # Large TCP buffers and windows (requires PSRAM) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 512000) + + # Large mailboxes for high throughput + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 512) + + # TCP connection limits + add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16) + add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16) + + # TCP optimizations + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MAXRTX", 12) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SYNMAXRTX", 6) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSS", 1436) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSL", 60000) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True) + else: + _LOGGER.info( + "Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes" + ) + # PSRAM not guaranteed - use more conservative, but still optimized settings + # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534) + add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) + add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) + if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None: cg.add_define("USE_NETWORK_IPV6", enable_ipv6) if enable_ipv6: diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 5e6b0dbd960..5ec6450cced 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -118,10 +118,10 @@ struct IPAddress { operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); } #endif - bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) - bool is_ip4() { return IP_IS_V4(&ip_addr_); } - bool is_ip6() { return IP_IS_V6(&ip_addr_); } - bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); } + bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr) + bool is_ip4() const { return IP_IS_V4(&ip_addr_); } + bool is_ip6() const { return IP_IS_V6(&ip_addr_); } + bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); } bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); } bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); } diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index cb8f8569add..5e741fd2445 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -85,7 +85,7 @@ network::IPAddresses get_ip_addresses() { return {}; } -const std::string &get_use_address() { +const char *get_use_address() { // Global component pointers are guaranteed to be set by component constructors when USE_* is defined #ifdef USE_ETHERNET return ethernet::global_eth_component->get_use_address(); @@ -105,8 +105,7 @@ const std::string &get_use_address() { #if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) // Fallback when no network component is defined (e.g., host platform) - static const std::string empty; - return empty; + return ""; #endif } diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index b4a92f8bee0..3dc12232aa8 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -12,7 +12,7 @@ bool is_connected(); /// Return whether the network is disabled (only wifi for now) bool is_disabled(); /// Get the active network hostname -const std::string &get_use_address(); +const char *get_use_address(); IPAddresses get_ip_addresses(); } // namespace network diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index c718355af85..8e85e15823e 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -55,7 +55,7 @@ template class NextionSetBrightnessAction : public Action TEMPLATABLE_VALUE(float, brightness) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_brightness(this->brightness_.value(x...)); this->component_->set_backlight_brightness(this->brightness_.value(x...)); } @@ -74,7 +74,7 @@ template class NextionPublishFloatAction : public Action TEMPLATABLE_VALUE(bool, publish_state) TEMPLATABLE_VALUE(bool, send_to_nextion) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...), this->send_to_nextion_.value(x...)); } @@ -97,7 +97,7 @@ template class NextionPublishTextAction : public Action { TEMPLATABLE_VALUE(bool, publish_state) TEMPLATABLE_VALUE(bool, send_to_nextion) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...), this->send_to_nextion_.value(x...)); } @@ -120,7 +120,7 @@ template class NextionPublishBoolAction : public Action { TEMPLATABLE_VALUE(bool, publish_state) TEMPLATABLE_VALUE(bool, send_to_nextion) - void play(Ts... x) override { + void play(const Ts &...x) override { this->component_->set_state(this->state_.value(x...), this->publish_state_.value(x...), this->send_to_nextion_.value(x...)); } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c078ab9d568..61068b52fc4 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -9,6 +9,7 @@ #include "esphome/components/uart/uart.h" #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 @@ -31,7 +32,7 @@ namespace nextion { class Nextion; class NextionComponentBase; -using nextion_writer_t = std::function; +using nextion_writer_t = display::DisplayWriter; static const std::string COMMAND_DELIMITER{static_cast(255), static_cast(255), static_cast(255)}; @@ -1471,7 +1472,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager touch_callback_{}; CallbackManager buffer_overflow_callback_{}; - optional writer_; + nextion_writer_t writer_; optional brightness_; #ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index ace324c1f56..03927e8ea27 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -25,6 +25,7 @@ from esphome.const import ( CONF_FRAMEWORK, CONF_ID, CONF_RESET_PIN, + CONF_VOLTAGE, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, @@ -102,6 +103,11 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52") DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component) CONF_DFU = "dfu" +CONF_DCDC = "dcdc" +CONF_REG0 = "reg0" +CONF_UICR_ERASE = "uicr_erase" + +VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3] CONFIG_SCHEMA = cv.All( _detect_bootloader, @@ -116,6 +122,16 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, } ), + cv.Optional(CONF_DCDC, default=True): cv.boolean, + cv.Optional(CONF_REG0): cv.Schema( + { + cv.Required(CONF_VOLTAGE): cv.All( + cv.voltage, + cv.one_of(*VOLTAGE_LEVELS, float=True), + ), + cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, + } + ), } ), ) @@ -182,6 +198,13 @@ async def to_code(config: ConfigType) -> None: if dfu_config := config.get(CONF_DFU): CORE.add_job(_dfu_to_code, dfu_config) + zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC]) + + if reg0_config := config.get(CONF_REG0): + value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE]) + cg.add_define("USE_NRF52_REG0_VOUT", value) + if reg0_config[CONF_UICR_ERASE]: + cg.add_define("USE_NRF52_UICR_ERASE") @coroutine_with_priority(CoroPriority.DIAGNOSTICS) diff --git a/esphome/components/nrf52/uicr.cpp b/esphome/components/nrf52/uicr.cpp new file mode 100644 index 00000000000..4c0beeb503b --- /dev/null +++ b/esphome/components/nrf52/uicr.cpp @@ -0,0 +1,121 @@ +#include "esphome/core/defines.h" + +#ifdef USE_NRF52_REG0_VOUT +#include +#include +#include + +extern "C" { +void nvmc_config(uint32_t mode); +void nvmc_wait(); +nrfx_err_t nrfx_nvmc_uicr_erase(); +} + +namespace esphome::nrf52 { + +enum class StatusFlags : uint8_t { + OK = 0x00, + NEED_RESET = 0x01, + NEED_ERASE = 0x02, +}; + +constexpr StatusFlags &operator|=(StatusFlags &a, StatusFlags b) { + a = static_cast(static_cast(a) | static_cast(b)); + return a; +} + +constexpr bool operator&(StatusFlags a, StatusFlags b) { + return (static_cast(a) & static_cast(b)) != 0; +} + +static bool regout0_ok() { + return (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) == (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos); +} + +static StatusFlags set_regout0() { + /* If the board is powered from USB (high voltage mode), + * GPIO output voltage is set to 1.8 volts by default. + */ + if (!regout0_ok()) { + nvmc_config(NVMC_CONFIG_WEN_Wen); + NRF_UICR->REGOUT0 = + (NRF_UICR->REGOUT0 & ~((uint32_t) UICR_REGOUT0_VOUT_Msk)) | (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos); + nvmc_wait(); + nvmc_config(NVMC_CONFIG_WEN_Ren); + return regout0_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE; + } + return StatusFlags::OK; +} + +#ifndef USE_BOOTLOADER_MCUBOOT +// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/6a9a6a3e6d0f86918e9286188426a279976645bd/lib/sdk11/components/libraries/bootloader_dfu/dfu_types.h#L61 +constexpr uint32_t BOOTLOADER_REGION_START = 0x000F4000; +constexpr uint32_t BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS = 0x000FE000; + +static bool bootloader_ok() { + return NRF_UICR->NRFFW[0] == BOOTLOADER_REGION_START && NRF_UICR->NRFFW[1] == BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS; +} + +static StatusFlags fix_bootloader() { + if (!bootloader_ok()) { + nvmc_config(NVMC_CONFIG_WEN_Wen); + NRF_UICR->NRFFW[0] = BOOTLOADER_REGION_START; + NRF_UICR->NRFFW[1] = BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS; + nvmc_wait(); + nvmc_config(NVMC_CONFIG_WEN_Ren); + return bootloader_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE; + } + return StatusFlags::OK; +} +#endif + +#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] + +static StatusFlags set_uicr() { + StatusFlags status = StatusFlags::OK; +#ifndef USE_BOOTLOADER_MCUBOOT + if (BOOTLOADER_VERSION_REGISTER <= 0x902) { +#ifdef CONFIG_PRINTK + printk("cannot control regout0 for %#x\n", BOOTLOADER_VERSION_REGISTER); +#endif + } else +#endif + { + status |= set_regout0(); + } +#ifndef USE_BOOTLOADER_MCUBOOT + status |= fix_bootloader(); +#endif + return status; +} + +static int board_esphome_init() { + StatusFlags status = set_uicr(); + +#ifdef USE_NRF52_UICR_ERASE + if (status & StatusFlags::NEED_ERASE) { + nrfx_err_t ret = nrfx_nvmc_uicr_erase(); + if (ret != NRFX_SUCCESS) { +#ifdef CONFIG_PRINTK + printk("nrfx_nvmc_uicr_erase failed %d\n", ret); +#endif + } else { + status |= set_uicr(); + } + } +#endif + + if (status & StatusFlags::NEED_RESET) { + /* a reset is required for changes to take effect */ + NVIC_SystemReset(); + } + + return 0; +} +} // namespace esphome::nrf52 + +static int board_esphome_init() { return esphome::nrf52::board_esphome_init(); } + +SYS_INIT(board_esphome_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); + +#endif diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 33f0f9727e5..79eba883c47 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -19,7 +19,7 @@ template class NumberSetAction : public Action { NumberSetAction(Number *number) : number_(number) {} TEMPLATABLE_VALUE(float, value) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->number_->make_call(); call.set_value(this->value_.value(x...)); call.perform(); @@ -35,7 +35,7 @@ template class NumberOperationAction : public Action { TEMPLATABLE_VALUE(NumberOperation, operation) TEMPLATABLE_VALUE(bool, cycle) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->number_->make_call(); call.with_operation(this->operation_.value(x...)); if (this->cycle_.has_value()) { @@ -74,7 +74,7 @@ template class NumberInRangeCondition : public Condition void set_min(float min) { this->min_ = min; } void set_max(float max) { this->max_ = max; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { const float state = this->parent_->state; if (std::isnan(this->min_)) { return state <= this->max_; diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index da08faf6558..f12e0e9e1e1 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -1,4 +1,6 @@ #include "number.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -32,6 +34,9 @@ void Number::publish_state(float state) { this->state = state; ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); this->state_callback_.call(state); +#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_number_update(this); +#endif } void Number::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/one_wire/__init__.py b/esphome/components/one_wire/__init__.py index 6d95b8fd33e..e12cca3e27f 100644 --- a/esphome/components/one_wire/__init__.py +++ b/esphome/components/one_wire/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ADDRESS +from esphome.const import CONF_ADDRESS, CONF_INDEX CODEOWNERS = ["@ssieb"] @@ -21,7 +21,8 @@ def one_wire_device_schema(): return cv.Schema( { cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus), - cv.Optional(CONF_ADDRESS): cv.hex_uint64_t, + cv.Exclusive(CONF_ADDRESS, "index_or_address"): cv.hex_uint64_t, + cv.Exclusive(CONF_INDEX, "index_or_address"): cv.uint8_t, } ) @@ -37,3 +38,5 @@ async def register_one_wire_device(var, config): cg.add(var.set_one_wire_bus(parent)) if (address := config.get(CONF_ADDRESS)) is not None: cg.add(var.set_address(address)) + if (index := config.get(CONF_INDEX)) is not None: + cg.add(var.set_index(index)) diff --git a/esphome/components/one_wire/one_wire.cpp b/esphome/components/one_wire/one_wire.cpp index 96e6145f63f..fd139d0ddc1 100644 --- a/esphome/components/one_wire/one_wire.cpp +++ b/esphome/components/one_wire/one_wire.cpp @@ -18,10 +18,20 @@ bool OneWireDevice::send_command_(uint8_t cmd) { return true; } -bool OneWireDevice::check_address_() { +bool OneWireDevice::check_address_or_index_() { if (this->address_ != 0) return true; auto devices = this->bus_->get_devices(); + + if (this->index_ != INDEX_NOT_SET) { + if (this->index_ >= devices.size()) { + ESP_LOGE(TAG, "Index %d out of range, only %d devices found", this->index_, devices.size()); + return false; + } + this->address_ = devices[this->index_]; + return true; + } + if (devices.empty()) { ESP_LOGE(TAG, "No devices, can't auto-select address"); return false; diff --git a/esphome/components/one_wire/one_wire.h b/esphome/components/one_wire/one_wire.h index e83c6e81e8e..f6a956a92c7 100644 --- a/esphome/components/one_wire/one_wire.h +++ b/esphome/components/one_wire/one_wire.h @@ -17,6 +17,8 @@ class OneWireDevice { /// @param address of the device void set_address(uint64_t address) { this->address_ = address; } + void set_index(uint8_t index) { this->index_ = index; } + /// @brief store the pointer to the OneWireBus to use /// @param bus pointer to the OneWireBus object void set_one_wire_bus(OneWireBus *bus) { this->bus_ = bus; } @@ -25,13 +27,16 @@ class OneWireDevice { const std::string &get_address_name(); protected: + static constexpr uint8_t INDEX_NOT_SET = 255; + uint64_t address_{0}; + uint8_t index_{INDEX_NOT_SET}; OneWireBus *bus_{nullptr}; ///< pointer to OneWireBus instance std::string address_name_; /// @brief find an address if necessary /// should be called from setup - bool check_address_(); + bool check_address_or_index_(); /// @brief send command on the bus /// @param cmd command to send diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 3326cbe8d6b..12d409ca29b 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -207,7 +207,7 @@ template class OnlineImageSetUrlAction : public Action { OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, url) TEMPLATABLE_VALUE(bool, update) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_url(this->url_.value(x...)); if (this->update_.value(x...)) { this->parent_->update(); @@ -221,7 +221,7 @@ template class OnlineImageSetUrlAction : public Action { template class OnlineImageReleaseAction : public Action { public: OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->release(); } + void play(const Ts &...x) override { this->parent_->release(); } protected: OnlineImage *parent_; diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 572ec144d49..e3ad3ed76ce 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -9,7 +9,7 @@ from esphome.components.esp32 import ( from esphome.components.mdns import MDNSComponent, enable_mdns_storage import esphome.config_validation as cv from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS -from esphome.core import CORE +from esphome.core import CORE, TimePeriodMilliseconds import esphome.final_validate as fv from esphome.types import ConfigType @@ -22,6 +22,7 @@ from .const import ( CONF_NETWORK_KEY, CONF_NETWORK_NAME, CONF_PAN_ID, + CONF_POLL_PERIOD, CONF_PSKC, CONF_SRP_ID, CONF_TLV, @@ -89,7 +90,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5) - # TODO: Add suport for sleepy end devices + # TODO: Add suport for synchronized sleepy end devices (SSED) add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True) @@ -113,6 +114,17 @@ _CONNECTION_SCHEMA = cv.Schema( def _validate(config: ConfigType) -> ConfigType: if CONF_USE_ADDRESS not in config: config[CONF_USE_ADDRESS] = f"{CORE.name}.local" + device_type = config.get(CONF_DEVICE_TYPE) + poll_period = config.get(CONF_POLL_PERIOD) + if ( + device_type == "FTD" + and poll_period + and poll_period > TimePeriodMilliseconds(milliseconds=0) + ): + raise cv.Invalid( + f"{CONF_POLL_PERIOD} can only be used with {CONF_DEVICE_TYPE}: MTD" + ) + return config @@ -135,6 +147,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FORCE_DATASET): cv.boolean, cv.Optional(CONF_TLV): cv.string_strict, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds, } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), @@ -167,6 +180,8 @@ async def to_code(config): ot = cg.new_Pvariable(config[CONF_ID]) cg.add(ot.set_use_address(config[CONF_USE_ADDRESS])) await cg.register_component(ot, config) + if (poll_period := config.get(CONF_POLL_PERIOD)) is not None: + cg.add(ot.set_poll_period(poll_period)) srp = cg.new_Pvariable(config[CONF_SRP_ID]) mdns_component = await cg.get_variable(config[CONF_MDNS_ID]) diff --git a/esphome/components/openthread/const.py b/esphome/components/openthread/const.py index 7a6ffb2df4b..f0274a8c9e5 100644 --- a/esphome/components/openthread/const.py +++ b/esphome/components/openthread/const.py @@ -6,6 +6,7 @@ CONF_MESH_LOCAL_PREFIX = "mesh_local_prefix" CONF_NETWORK_NAME = "network_name" CONF_NETWORK_KEY = "network_key" CONF_PAN_ID = "pan_id" +CONF_POLL_PERIOD = "poll_period" CONF_PSKC = "pskc" CONF_SRP_ID = "srp_id" CONF_TLV = "tlv" diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index db909e6b1ff..721ab89326c 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -29,6 +29,23 @@ OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines- OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; } +void OpenThreadComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Open Thread:"); +#if CONFIG_OPENTHREAD_FTD + ESP_LOGCONFIG(TAG, " Device Type: FTD"); +#elif CONFIG_OPENTHREAD_MTD + ESP_LOGCONFIG(TAG, " Device Type: MTD"); + // TBD: Synchronized Sleepy End Device + if (this->poll_period > 0) { + ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)"); + uint32_t duration = this->poll_period / 1000; + ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration); + } else { + ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); + } +#endif +} + bool OpenThreadComponent::is_connected() { auto lock = InstanceLock::try_acquire(100); if (!lock) { @@ -254,9 +271,9 @@ void OpenThreadComponent::on_factory_reset(std::function callback) { // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const std::string &OpenThreadComponent::get_use_address() const { return this->use_address_; } +const char *OpenThreadComponent::get_use_address() const { return this->use_address_; } -void OpenThreadComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +void OpenThreadComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } } // namespace openthread } // namespace esphome diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 19dbeb46283..546128b3666 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -22,6 +22,7 @@ class OpenThreadComponent : public Component { public: OpenThreadComponent(); ~OpenThreadComponent(); + void dump_config() override; void setup() override; bool teardown() override; float get_setup_priority() const override { return setup_priority::WIFI; } @@ -33,15 +34,25 @@ class OpenThreadComponent : public Component { void on_factory_reset(std::function callback); void defer_factory_reset_external_callback(); - const std::string &get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); +#if CONFIG_OPENTHREAD_MTD + void set_poll_period(uint32_t poll_period) { this->poll_period = poll_period; } +#endif protected: std::optional get_omr_address_(InstanceLock &lock); bool teardown_started_{false}; bool teardown_complete_{false}; std::function factory_reset_external_callback_; - std::string use_address_; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; +#if CONFIG_OPENTHREAD_MTD + uint32_t poll_period{0}; +#endif }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index b11b7ad34a3..72dc521091c 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -105,6 +105,32 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION + otLinkModeConfig link_mode_config = {0}; +#if CONFIG_OPENTHREAD_FTD + link_mode_config.mRxOnWhenIdle = true; + link_mode_config.mDeviceType = true; + link_mode_config.mNetworkData = true; +#elif CONFIG_OPENTHREAD_MTD + if (this->poll_period > 0) { + if (otLinkSetPollPeriod(esp_openthread_get_instance(), this->poll_period) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); + } + uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); + ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + } + link_mode_config.mRxOnWhenIdle = this->poll_period == 0; + link_mode_config.mDeviceType = false; + link_mode_config.mNetworkData = false; +#endif + + if (otThreadSetLinkMode(esp_openthread_get_instance(), link_mode_config) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set OpenThread linkmode."); + } + link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance()); + ESP_LOGD(TAG, "Link Mode Device Type: %s", link_mode_config.mDeviceType ? "true" : "false"); + ESP_LOGD(TAG, "Link Mode Network Data: %s", link_mode_config.mNetworkData ? "true" : "false"); + ESP_LOGD(TAG, "Link Mode RX On When Idle: %s", link_mode_config.mRxOnWhenIdle ? "true" : "false"); + // Run the main loop #if CONFIG_OPENTHREAD_CLI esp_openthread_cli_create_task(); diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index de84bb91ca2..32793781292 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -12,7 +12,7 @@ template class TurnOffAction : public Action { public: TurnOffAction(BinaryOutput *output) : output_(output) {} - void play(Ts... x) override { this->output_->turn_off(); } + void play(const Ts &...x) override { this->output_->turn_off(); } protected: BinaryOutput *output_; @@ -22,7 +22,7 @@ template class TurnOnAction : public Action { public: TurnOnAction(BinaryOutput *output) : output_(output) {} - void play(Ts... x) override { this->output_->turn_on(); } + void play(const Ts &...x) override { this->output_->turn_on(); } protected: BinaryOutput *output_; @@ -34,7 +34,7 @@ template class SetLevelAction : public Action { TEMPLATABLE_VALUE(float, level) - void play(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_level(this->level_.value(x...)); } protected: FloatOutput *output_; @@ -46,7 +46,7 @@ template class SetMinPowerAction : public Action { TEMPLATABLE_VALUE(float, min_power) - void play(Ts... x) override { this->output_->set_min_power(this->min_power_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_min_power(this->min_power_.value(x...)); } protected: FloatOutput *output_; @@ -58,7 +58,7 @@ template class SetMaxPowerAction : public Action { TEMPLATABLE_VALUE(float, max_power) - void play(Ts... x) override { this->output_->set_max_power(this->max_power_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_max_power(this->max_power_.value(x...)); } protected: FloatOutput *output_; diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index cb987c6129e..f38b60b55d4 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -23,7 +23,7 @@ void PCF85063Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pcf85063/pcf85063.h b/esphome/components/pcf85063/pcf85063.h index 1a3fd704e57..b7034d4f00f 100644 --- a/esphome/components/pcf85063/pcf85063.h +++ b/esphome/components/pcf85063/pcf85063.h @@ -85,12 +85,12 @@ class PCF85063Component : public time::RealTimeClock, public i2c::I2CDevice { template class WriteAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->write_time(); } + void play(const Ts &...x) override { this->parent_->write_time(); } }; template class ReadAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->read_time(); } + void play(const Ts &...x) override { this->parent_->read_time(); } }; } // namespace pcf85063 } // namespace esphome diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index 27020378a6a..2090936bb69 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -23,7 +23,7 @@ void PCF8563Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); + RealTimeClock::dump_config(); } float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h index b6832efe72d..81aa816b42d 100644 --- a/esphome/components/pcf8563/pcf8563.h +++ b/esphome/components/pcf8563/pcf8563.h @@ -113,12 +113,12 @@ class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { template class WriteAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->write_time(); } + void play(const Ts &...x) override { this->parent_->write_time(); } }; template class ReadAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->read_time(); } + void play(const Ts &...x) override { this->parent_->read_time(); } }; } // namespace pcf8563 } // namespace esphome diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 1a09ffdd20f..dc0a92efed5 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -109,7 +109,7 @@ template class PIDAutotuneAction : public Action { void set_positive_output(float positive_output) { positive_output_ = positive_output; } void set_negative_output(float negative_output) { negative_output_ = negative_output; } - void play(Ts... x) { + void play(const Ts &...x) { auto tuner = make_unique(); tuner->set_noiseband(this->noiseband_); tuner->set_output_negative(this->negative_output_); @@ -128,7 +128,7 @@ template class PIDResetIntegralTermAction : public Action public: PIDResetIntegralTermAction(PIDClimate *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->reset_integral_term(); } + void play(const Ts &...x) { this->parent_->reset_integral_term(); } protected: PIDClimate *parent_; @@ -138,7 +138,7 @@ template class PIDSetControlParametersAction : public Actionkp_.value(x...); auto ki = this->ki_.value(x...); auto kd = this->kd_.value(x...); diff --git a/esphome/components/pipsolar/output/pipsolar_output.h b/esphome/components/pipsolar/output/pipsolar_output.h index 29b2d116f2c..b4b8000962c 100644 --- a/esphome/components/pipsolar/output/pipsolar_output.h +++ b/esphome/components/pipsolar/output/pipsolar_output.h @@ -32,7 +32,7 @@ template class SetOutputAction : public Action { TEMPLATABLE_VALUE(float, level) - void play(Ts... x) override { this->output_->set_value(this->level_.value(x...)); } + void play(const Ts &...x) override { this->output_->set_value(this->level_.value(x...)); } protected: PipsolarOutput *output_; diff --git a/esphome/components/pmwcs3/pmwcs3.h b/esphome/components/pmwcs3/pmwcs3.h index d60f9d1f61c..d63c5165864 100644 --- a/esphome/components/pmwcs3/pmwcs3.h +++ b/esphome/components/pmwcs3/pmwcs3.h @@ -38,7 +38,7 @@ template class PMWCS3AirCalibrationAction : public Action public: PMWCS3AirCalibrationAction(PMWCS3Component *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->air_calibration(); } + void play(const Ts &...x) override { this->parent_->air_calibration(); } protected: PMWCS3Component *parent_; @@ -48,7 +48,7 @@ template class PMWCS3WaterCalibrationAction : public Actionparent_->water_calibration(); } + void play(const Ts &...x) override { this->parent_->water_calibration(); } protected: PMWCS3Component *parent_; @@ -59,7 +59,7 @@ template class PMWCS3NewI2cAddressAction : public Action PMWCS3NewI2cAddressAction(PMWCS3Component *parent) : parent_(parent) {} TEMPLATABLE_VALUE(int, new_address) - void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } + void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } protected: PMWCS3Component *parent_; diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index c8e9a400086..eeb15648fbf 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -143,7 +143,7 @@ class PN532OnFinishedWriteTrigger : public Trigger<> { template class PN532IsWritingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_writing(); } + bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; } // namespace pn532 diff --git a/esphome/components/pn7150/automation.h b/esphome/components/pn7150/automation.h index aebb1b7573e..21329a998a2 100644 --- a/esphome/components/pn7150/automation.h +++ b/esphome/components/pn7150/automation.h @@ -23,42 +23,42 @@ class PN7150OnFinishedWriteTrigger : public Trigger<> { template class PN7150IsWritingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_writing(); } + bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; template class EmulationOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_off(); } }; template class EmulationOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_on(); } }; template class PollingOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_off(); } + void play(const Ts &...x) override { this->parent_->set_polling_off(); } }; template class PollingOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_on(); } + void play(const Ts &...x) override { this->parent_->set_polling_on(); } }; template class SetCleanModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->clean_mode(); } + void play(const Ts &...x) override { this->parent_->clean_mode(); } }; template class SetFormatModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->format_mode(); } + void play(const Ts &...x) override { this->parent_->format_mode(); } }; template class SetReadModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->read_mode(); } + void play(const Ts &...x) override { this->parent_->read_mode(); } }; template class SetEmulationMessageAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } @@ -68,14 +68,14 @@ template class SetWriteMessageAction : public Action, pub TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_write_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } }; template class SetWriteModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->write_mode(); } + void play(const Ts &...x) override { this->parent_->write_mode(); } }; } // namespace pn7150 diff --git a/esphome/components/pn7160/automation.h b/esphome/components/pn7160/automation.h index 854fb116848..08148c23114 100644 --- a/esphome/components/pn7160/automation.h +++ b/esphome/components/pn7160/automation.h @@ -23,42 +23,42 @@ class PN7160OnFinishedWriteTrigger : public Trigger<> { template class PN7160IsWritingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_writing(); } + bool check(const Ts &...x) override { return this->parent_->is_writing(); } }; template class EmulationOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_off(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_off(); } }; template class EmulationOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_tag_emulation_on(); } + void play(const Ts &...x) override { this->parent_->set_tag_emulation_on(); } }; template class PollingOffAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_off(); } + void play(const Ts &...x) override { this->parent_->set_polling_off(); } }; template class PollingOnAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->set_polling_on(); } + void play(const Ts &...x) override { this->parent_->set_polling_on(); } }; template class SetCleanModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->clean_mode(); } + void play(const Ts &...x) override { this->parent_->clean_mode(); } }; template class SetFormatModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->format_mode(); } + void play(const Ts &...x) override { this->parent_->format_mode(); } }; template class SetReadModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->read_mode(); } + void play(const Ts &...x) override { this->parent_->read_mode(); } }; template class SetEmulationMessageAction : public Action, public Parented { TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_emulation_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } @@ -68,14 +68,14 @@ template class SetWriteMessageAction : public Action, pub TEMPLATABLE_VALUE(std::string, message) TEMPLATABLE_VALUE(bool, include_android_app_record) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_tag_write_message(this->message_.optional_value(x...), this->include_android_app_record_.optional_value(x...)); } }; template class SetWriteModeAction : public Action, public Parented { - void play(Ts... x) override { this->parent_->write_mode(); } + void play(const Ts &...x) override { this->parent_->write_mode(); } }; } // namespace pn7160 diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 6e7ed6f79fc..5cfcacf0cbb 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -158,7 +158,7 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",unit=\"")); - stream->print(obj->get_unit_of_measurement().c_str()); + stream->print(obj->get_unit_of_measurement_ref().c_str()); stream->print(ESPHOME_F("\"} ")); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); stream->print(ESPHOME_F("\n")); diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index 8e4f9d7eac7..c50c5998550 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -1,6 +1,8 @@ import logging +import textwrap import esphome.codegen as cg +from esphome.components.const import CONF_IGNORE_NOT_FOUND from esphome.components.esp32 import ( CONF_CPU_FREQUENCY, CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, @@ -33,6 +35,9 @@ DOMAIN = "psram" DEPENDENCIES = [PLATFORM_ESP32] +# PSRAM availability tracking for cross-component coordination +KEY_PSRAM_GUARANTEED = "psram_guaranteed" + _LOGGER = logging.getLogger(__name__) psram_ns = cg.esphome_ns.namespace(DOMAIN) @@ -69,6 +74,23 @@ def supported() -> bool: return variant in SPIRAM_MODES +def is_guaranteed() -> bool: + """Check if PSRAM is guaranteed to be available. + + Returns True when PSRAM is configured with both 'disabled: false' and + 'ignore_not_found: false', meaning the device will fail to boot if PSRAM + is not found. This ensures safe use of high buffer configurations that + depend on PSRAM. + + This function should be called during code generation (to_code phase) by + components that need to know PSRAM availability for configuration decisions. + + Returns: + bool: True if PSRAM is guaranteed, False otherwise + """ + return CORE.data.get(KEY_PSRAM_GUARANTEED, False) + + def validate_psram_mode(config): esp32_config = fv.full_config.get()[PLATFORM_ESP32] if config[CONF_SPEED] == "120MHZ": @@ -104,6 +126,17 @@ def get_config_schema(config): if not speeds: raise cv.Invalid("PSRAM is not supported on this chip") modes = SPIRAM_MODES[variant] + if CONF_MODE not in config and len(modes) != 1: + raise ( + cv.Invalid( + textwrap.dedent( + f""" + {variant} requires PSRAM mode selection; one of {", ".join(modes)} + Selection of the wrong mode for the board will cause a runtime failure to initialise PSRAM + """ + ) + ) + ) return cv.Schema( { cv.GenerateID(): cv.declare_id(PsramComponent), @@ -111,13 +144,29 @@ def get_config_schema(config): cv.Optional(CONF_ENABLE_ECC, default=False): cv.boolean, cv.Optional(CONF_SPEED, default=speeds[0]): cv.one_of(*speeds, upper=True), cv.Optional(CONF_DISABLED, default=False): cv.boolean, + cv.Optional(CONF_IGNORE_NOT_FOUND, default=True): cv.boolean, } )(config) CONFIG_SCHEMA = get_config_schema -FINAL_VALIDATE_SCHEMA = validate_psram_mode + +def _store_psram_guaranteed(config): + """Store PSRAM guaranteed status in CORE.data for other components. + + PSRAM is "guaranteed" when it will fail if not found, ensuring safe use + of high buffer configurations in network/wifi components. + + Called during final validation to ensure the flag is available + before any to_code() functions run. + """ + psram_guaranteed = not config[CONF_DISABLED] and not config[CONF_IGNORE_NOT_FOUND] + CORE.data[KEY_PSRAM_GUARANTEED] = psram_guaranteed + return config + + +FINAL_VALIDATE_SCHEMA = cv.All(validate_psram_mode, _store_psram_guaranteed) async def to_code(config): @@ -135,7 +184,9 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_SPIRAM", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_USE", True) add_idf_sdkconfig_option("CONFIG_SPIRAM_USE_CAPS_ALLOC", True) - add_idf_sdkconfig_option("CONFIG_SPIRAM_IGNORE_NOTFOUND", True) + add_idf_sdkconfig_option( + "CONFIG_SPIRAM_IGNORE_NOTFOUND", config[CONF_IGNORE_NOT_FOUND] + ) add_idf_sdkconfig_option(f"CONFIG_SPIRAM_MODE_{SDK_MODES[config[CONF_MODE]]}", True) diff --git a/esphome/components/pulse_counter/automation.h b/esphome/components/pulse_counter/automation.h index d749540a954..0c0dc2552d1 100644 --- a/esphome/components/pulse_counter/automation.h +++ b/esphome/components/pulse_counter/automation.h @@ -14,7 +14,7 @@ template class SetTotalPulsesAction : public Action { TEMPLATABLE_VALUE(uint32_t, total_pulses) - void play(Ts... x) override { this->pulse_counter_->set_total_pulses(this->total_pulses_.value(x...)); } + void play(const Ts &...x) override { this->pulse_counter_->set_total_pulses(this->total_pulses_.value(x...)); } protected: PulseCounterSensor *pulse_counter_; diff --git a/esphome/components/pulse_meter/automation.h b/esphome/components/pulse_meter/automation.h index 3112ded6802..bf0768b7af5 100644 --- a/esphome/components/pulse_meter/automation.h +++ b/esphome/components/pulse_meter/automation.h @@ -14,7 +14,7 @@ template class SetTotalPulsesAction : public Action { TEMPLATABLE_VALUE(uint32_t, total_pulses) - void play(Ts... x) override { this->pulse_meter_->set_total_pulses(this->total_pulses_.value(x...)); } + void play(const Ts &...x) override { this->pulse_meter_->set_total_pulses(this->total_pulses_.value(x...)); } protected: PulseMeterSensor *pulse_meter_; diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index c7fc5234206..8637506baef 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/display/display.h" #include @@ -29,7 +30,7 @@ enum UNIT { UNIT_DEG_E, ///< show "°E" }; -using pvvx_writer_t = std::function; +using pvvx_writer_t = display::DisplayWriter; class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { public: @@ -126,7 +127,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { esp32_ble_tracker::ESPBTUUID char_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw("00001f1f-0000-1000-8000-00805f9b34fb"); - optional writer_{}; + pvvx_writer_t writer_{}; }; } // namespace pvvx_mithermometer diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index 7a229b49ce6..e5b96115f93 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -43,7 +43,7 @@ template class ResetEnergyAction : public Action { public: ResetEnergyAction(PZEMAC *pzemac) : pzemac_(pzemac) {} - void play(Ts... x) override { this->pzemac_->reset_energy_(); } + void play(const Ts &...x) override { this->pzemac_->reset_energy_(); } protected: PZEMAC *pzemac_; diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index b91ab4c0a56..2e6c26a10cf 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -36,7 +36,7 @@ template class ResetEnergyAction : public Action { public: ResetEnergyAction(PZEMDC *pzemdc) : pzemdc_(pzemdc) {} - void play(Ts... x) override { this->pzemdc_->reset_energy(); } + void play(const Ts &...x) override { this->pzemdc_->reset_energy(); } protected: PZEMDC *pzemdc_; diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 8d735ea563a..d24d24b0007 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -39,7 +39,7 @@ from esphome.const import ( CONF_WAND_ID, CONF_ZERO, ) -from esphome.core import coroutine +from esphome.core import ID, coroutine from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.util import Registry, SimpleRegistry @@ -2104,7 +2104,9 @@ async def abbwelcome_action(var, config, args): ) cg.add(var.set_data_template(template_)) else: - cg.add(var.set_data_static(data_)) + arr_id = ID(f"{var.base}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data_)) + cg.add(var.set_data_static(arr, len(data_))) # Mirage diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index b258bd920b3..4b922eb2f16 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -214,10 +214,13 @@ template class ABBWelcomeAction : public RemoteTransmitterAction TEMPLATABLE_VALUE(uint8_t, message_type) TEMPLATABLE_VALUE(uint8_t, message_id) TEMPLATABLE_VALUE(bool, auto_message_id) - void set_data_static(std::vector data) { data_static_ = std::move(data); } - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - has_data_func_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } void encode(RemoteTransmitData *dst, Ts... x) override { ABBWelcomeData data; @@ -228,19 +231,25 @@ template class ABBWelcomeAction : public RemoteTransmitterAction data.set_message_type(this->message_type_.value(x...)); data.set_message_id(this->message_id_.value(x...)); data.auto_message_id = this->auto_message_id_.value(x...); - if (has_data_func_) { - data.set_data(this->data_func_(x...)); + std::vector data_vec; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data_vec.assign(this->data_.data, this->data_.data + this->len_); } else { - data.set_data(this->data_static_); + // Template mode: call function + data_vec = this->data_.func(x...); } + data.set_data(data_vec); data.finalize(); ABBWelcomeProtocol().encode(dst, data); } protected: - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; - bool has_data_func_{false}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; } // namespace remote_base diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 35fd7822487..9fbc9e85baf 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -71,6 +71,7 @@ static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0 static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; static const uint16_t PRONTO_DEFAULT_GAP = 45000; static const uint16_t MARK_EXCESS_MICROS = 20; +static constexpr size_t PRONTO_LOG_CHUNK_SIZE = 230; static uint16_t to_frequency_k_hz(uint16_t code) { if (code == 0) @@ -225,18 +226,18 @@ optional ProntoProtocol::decode(RemoteReceiveData src) { } void ProntoProtocol::dump(const ProntoData &data) { - std::string rest; - - rest = data.data; ESP_LOGI(TAG, "Received Pronto: data="); - while (true) { - ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str()); - if (rest.size() > 230) { - rest = rest.substr(230); - } else { - break; - } - } + + const char *ptr = data.data.c_str(); + size_t remaining = data.data.size(); + + // Log in chunks, always logging at least once (even for empty string) + do { + size_t chunk_size = remaining < PRONTO_LOG_CHUNK_SIZE ? remaining : PRONTO_LOG_CHUNK_SIZE; + ESP_LOGI(TAG, "%.*s", (int) chunk_size, ptr); + ptr += chunk_size; + remaining -= chunk_size; + } while (remaining > 0); } } // namespace remote_base diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index 9b671e611f4..941b6aab426 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -42,17 +42,20 @@ class RawTrigger : public Trigger, public Component, public RemoteRe template class RawAction : public RemoteTransmitterActionBase { public: - void set_code_template(std::function func) { this->code_func_ = func; } + void set_code_template(RawTimings (*func)(Ts...)) { + this->code_.func = func; + this->len_ = -1; // Sentinel value indicates template mode + } void set_code_static(const int32_t *code, size_t len) { - this->code_static_ = code; - this->code_static_len_ = len; + this->code_.data = code; + this->len_ = len; // Length >= 0 indicates static mode } TEMPLATABLE_VALUE(uint32_t, carrier_frequency); void encode(RemoteTransmitData *dst, Ts... x) override { - if (this->code_static_ != nullptr) { - for (size_t i = 0; i < this->code_static_len_; i++) { - auto val = this->code_static_[i]; + if (this->len_ >= 0) { + for (size_t i = 0; i < static_cast(this->len_); i++) { + auto val = this->code_.data[i]; if (val < 0) { dst->space(static_cast(-val)); } else { @@ -60,15 +63,17 @@ template class RawAction : public RemoteTransmitterActionBaseset_data(this->code_func_(x...)); + dst->set_data(this->code_.func(x...)); } dst->set_carrier_frequency(this->carrier_frequency_.value(x...)); } protected: - std::function code_func_{nullptr}; - const int32_t *code_static_{nullptr}; - int32_t code_static_len_{0}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Code { + RawTimings (*func)(Ts...); + const int32_t *data; + } code_; }; class RawDumper : public RemoteReceiverDumperBase { diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index b740ba80850..2cb79bf5715 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -276,7 +276,7 @@ template class RemoteTransmitterActionBase : public RemoteTransm TEMPLATABLE_VALUE(uint32_t, send_wait) protected: - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->transmitter_->transmit(); this->encode(call.get_data(), x...); call.set_send_times(this->send_times_.value_or(x..., 1)); diff --git a/esphome/components/remote_transmitter/automation.h b/esphome/components/remote_transmitter/automation.h index 75b017ec61b..bee1d0be8ac 100644 --- a/esphome/components/remote_transmitter/automation.h +++ b/esphome/components/remote_transmitter/automation.h @@ -11,7 +11,7 @@ namespace remote_transmitter { template class DigitalWriteAction : public Action, public Parented { public: TEMPLATABLE_VALUE(bool, value) - void play(Ts... x) override { this->parent_->digital_write(this->value_.value(x...)); } + void play(const Ts &...x) override { this->parent_->digital_write(this->value_.value(x...)); } }; } // namespace remote_transmitter diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index fe6dd96b38b..d2f75c819dc 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -98,7 +98,7 @@ template class RFBridgeSendCodeAction : public Action { TEMPLATABLE_VALUE(uint16_t, high) TEMPLATABLE_VALUE(uint32_t, code) - void play(Ts... x) { + void play(const Ts &...x) { RFBridgeData data{}; data.sync = this->sync_.value(x...); data.low = this->low_.value(x...); @@ -118,7 +118,7 @@ template class RFBridgeSendAdvancedCodeAction : public Actionlength_.value(x...); data.protocol = this->protocol_.value(x...); @@ -134,7 +134,7 @@ template class RFBridgeLearnAction : public Action { public: RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->learn(); } + void play(const Ts &...x) { this->parent_->learn(); } protected: RFBridgeComponent *parent_; @@ -144,7 +144,7 @@ template class RFBridgeStartAdvancedSniffingAction : public Acti public: RFBridgeStartAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_advanced_sniffing(); } + void play(const Ts &...x) { this->parent_->start_advanced_sniffing(); } protected: RFBridgeComponent *parent_; @@ -154,7 +154,7 @@ template class RFBridgeStopAdvancedSniffingAction : public Actio public: RFBridgeStopAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->stop_advanced_sniffing(); } + void play(const Ts &...x) { this->parent_->stop_advanced_sniffing(); } protected: RFBridgeComponent *parent_; @@ -164,7 +164,7 @@ template class RFBridgeStartBucketSniffingAction : public Action public: RFBridgeStartBucketSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->start_bucket_sniffing(); } + void play(const Ts &...x) { this->parent_->start_bucket_sniffing(); } protected: RFBridgeComponent *parent_; @@ -175,7 +175,7 @@ template class RFBridgeSendRawAction : public Action { RFBridgeSendRawAction(RFBridgeComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, raw) - void play(Ts... x) { this->parent_->send_raw(this->raw_.value(x...)); } + void play(const Ts &...x) { this->parent_->send_raw(this->raw_.value(x...)); } protected: RFBridgeComponent *parent_; @@ -186,7 +186,7 @@ template class RFBridgeBeepAction : public Action { RFBridgeBeepAction(RFBridgeComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(uint16_t, duration) - void play(Ts... x) { this->parent_->beep(this->duration_.value(x...)); } + void play(const Ts &...x) { this->parent_->beep(this->duration_.value(x...)); } protected: RFBridgeComponent *parent_; diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index e88ee9152af..14442f0565f 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -114,7 +114,7 @@ template class RotaryEncoderSetValueAction : public Actionencoder_->set_value(this->value_.value(x...)); } + void play(const Ts &...x) override { this->encoder_->set_value(this->value_.value(x...)); } protected: RotaryEncoderSensor *encoder_; diff --git a/esphome/components/rp2040_pwm/rp2040_pwm.h b/esphome/components/rp2040_pwm/rp2040_pwm.h index e499e72b061..b82765b1c01 100644 --- a/esphome/components/rp2040_pwm/rp2040_pwm.h +++ b/esphome/components/rp2040_pwm/rp2040_pwm.h @@ -45,7 +45,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(RP2040PWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + void play(const Ts &...x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index b79f27e2e5c..65fcc207d48 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -35,9 +35,9 @@ void Rtttl::dump_config() { void Rtttl::play(std::string rtttl) { if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) { - int pos = this->rtttl_.find(':'); - auto name = this->rtttl_.substr(0, pos); - ESP_LOGW(TAG, "Already playing: %s", name.c_str()); + size_t pos = this->rtttl_.find(':'); + size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length(); + ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str()); return; } @@ -59,8 +59,7 @@ void Rtttl::play(std::string rtttl) { return; } - auto name = this->rtttl_.substr(0, this->position_); - ESP_LOGD(TAG, "Playing song %s", name.c_str()); + ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str()); // get default duration this->position_ = this->rtttl_.find("d=", this->position_); diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index d536c6c08e1..1e924a897c2 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -122,7 +122,7 @@ template class PlayAction : public Action { PlayAction(Rtttl *rtttl) : rtttl_(rtttl) {} TEMPLATABLE_VALUE(std::string, value) - void play(Ts... x) override { this->rtttl_->play(this->value_.value(x...)); } + void play(const Ts &...x) override { this->rtttl_->play(this->value_.value(x...)); } protected: Rtttl *rtttl_; @@ -130,12 +130,12 @@ template class PlayAction : public Action { template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class IsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_playing(); } + bool check(const Ts &...x) override { return this->parent_->is_playing(); } }; class FinishedPlaybackTrigger : public Trigger<> { diff --git a/esphome/components/rx8130/__init__.py b/esphome/components/rx8130/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/esphome/components/rx8130/rx8130.cpp b/esphome/components/rx8130/rx8130.cpp new file mode 100644 index 00000000000..ba092a48340 --- /dev/null +++ b/esphome/components/rx8130/rx8130.cpp @@ -0,0 +1,128 @@ +#include "rx8130.h" +#include "esphome/core/log.h" + +// https://download.epsondevice.com/td/pdf/app/RX8130CE_en.pdf + +namespace esphome { +namespace rx8130 { + +static const uint8_t RX8130_REG_SEC = 0x10; +static const uint8_t RX8130_REG_MIN = 0x11; +static const uint8_t RX8130_REG_HOUR = 0x12; +static const uint8_t RX8130_REG_WDAY = 0x13; +static const uint8_t RX8130_REG_MDAY = 0x14; +static const uint8_t RX8130_REG_MONTH = 0x15; +static const uint8_t RX8130_REG_YEAR = 0x16; +static const uint8_t RX8130_REG_EXTEN = 0x1C; +static const uint8_t RX8130_REG_FLAG = 0x1D; +static const uint8_t RX8130_REG_CTRL0 = 0x1E; +static const uint8_t RX8130_REG_CTRL1 = 0x1F; +static const uint8_t RX8130_REG_DIG_OFFSET = 0x30; +static const uint8_t RX8130_BIT_CTRL_STOP = 0x40; +static const uint8_t RX8130_BAT_FLAGS = 0x30; +static const uint8_t RX8130_CLEAR_FLAGS = 0x00; + +static const char *const TAG = "rx8130"; + +constexpr uint8_t bcd2dec(uint8_t val) { return (val >> 4) * 10 + (val & 0x0f); } +constexpr uint8_t dec2bcd(uint8_t val) { return ((val / 10) << 4) + (val % 10); } + +void RX8130Component::setup() { + // Set digital offset to disabled with no offset + if (this->write_register(RX8130_REG_DIG_OFFSET, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Disable wakeup timers + if (this->write_register(RX8130_REG_EXTEN, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Clear VLF flag in case there has been data loss + if (this->write_register(RX8130_REG_FLAG, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Clear test flag and disable interrupts + if (this->write_register(RX8130_REG_CTRL0, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Enable battery charging and switching + if (this->write_register(RX8130_REG_CTRL1, &RX8130_BAT_FLAGS, 1) != i2c::ERROR_OK) { + this->mark_failed(); + return; + } + // Clear STOP bit + this->stop_(false); +} + +void RX8130Component::update() { this->read_time(); } + +void RX8130Component::dump_config() { + ESP_LOGCONFIG(TAG, "RX8130:"); + LOG_I2C_DEVICE(this); + RealTimeClock::dump_config(); +} + +void RX8130Component::read_time() { + uint8_t date[7]; + if (this->read_register(RX8130_REG_SEC, date, 7) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + return; + } + ESPTime rtc_time{ + .second = bcd2dec(date[0] & 0x7f), + .minute = bcd2dec(date[1] & 0x7f), + .hour = bcd2dec(date[2] & 0x3f), + .day_of_week = bcd2dec(date[3] & 0x7f), + .day_of_month = bcd2dec(date[4] & 0x3f), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = bcd2dec(date[5] & 0x1f), + .year = static_cast(bcd2dec(date[6]) + 2000), + .is_dst = false, // not used + .timestamp = 0 // overwritten by recalc_timestamp_utc(false) + }; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month, + rtc_time.hour, rtc_time.minute, rtc_time.second); + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void RX8130Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + uint8_t buff[7]; + buff[0] = dec2bcd(now.second); + buff[1] = dec2bcd(now.minute); + buff[2] = dec2bcd(now.hour); + buff[3] = dec2bcd(now.day_of_week); + buff[4] = dec2bcd(now.day_of_month); + buff[5] = dec2bcd(now.month); + buff[6] = dec2bcd(now.year % 100); + this->stop_(true); + if (this->write_register(RX8130_REG_SEC, buff, 7) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + } else { + ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour, + now.minute, now.second); + } + this->stop_(false); +} + +void RX8130Component::stop_(bool stop) { + const uint8_t data = stop ? RX8130_BIT_CTRL_STOP : RX8130_CLEAR_FLAGS; + if (this->write_register(RX8130_REG_CTRL0, &data, 1) != i2c::ERROR_OK) { + this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + } +} + +} // namespace rx8130 +} // namespace esphome diff --git a/esphome/components/rx8130/rx8130.h b/esphome/components/rx8130/rx8130.h new file mode 100644 index 00000000000..6694c763cd5 --- /dev/null +++ b/esphome/components/rx8130/rx8130.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace rx8130 { + +class RX8130Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + void read_time(); + void write_time(); + /// Ensure RTC is initialized at the correct time in the setup sequence + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void stop_(bool stop); +}; + +template class WriteAction : public Action, public Parented { + public: + void play(const Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(const Ts... x) override { this->parent_->read_time(); } +}; + +} // namespace rx8130 +} // namespace esphome diff --git a/esphome/components/rx8130/time.py b/esphome/components/rx8130/time.py new file mode 100644 index 00000000000..cb0402bd323 --- /dev/null +++ b/esphome/components/rx8130/time.py @@ -0,0 +1,56 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import i2c, time +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@beormund"] +DEPENDENCIES = ["i2c"] +rx8130_ns = cg.esphome_ns.namespace("rx8130") +RX8130Component = rx8130_ns.class_("RX8130Component", time.RealTimeClock, i2c.I2CDevice) +WriteAction = rx8130_ns.class_("WriteAction", automation.Action) +ReadAction = rx8130_ns.class_("ReadAction", automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(RX8130Component), + } +).extend(i2c.i2c_device_schema(0x32)) + + +@automation.register_action( + "rx8130.write_time", + WriteAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(RX8130Component), + } + ), +) +async def rx8130_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "rx8130.read_time", + ReadAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(RX8130Component), + } + ), +) +async def rx8130_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +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) + await time.register_time(var, config) diff --git a/esphome/components/scd30/automation.h b/esphome/components/scd30/automation.h index 37b3bc16747..1f89e7c8159 100644 --- a/esphome/components/scd30/automation.h +++ b/esphome/components/scd30/automation.h @@ -9,7 +9,7 @@ namespace scd30 { template class ForceRecalibrationWithReference : public Action, public Parented { public: - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->value_.has_value()) { this->parent_->force_recalibration_with_reference(this->value_.value(x...)); } diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h index dc43e9eb560..6ce14685770 100644 --- a/esphome/components/scd4x/automation.h +++ b/esphome/components/scd4x/automation.h @@ -9,7 +9,7 @@ namespace scd4x { template class PerformForcedCalibrationAction : public Action, public Parented { public: - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->value_.has_value()) { this->parent_->perform_forced_calibration(this->value_.value(x...)); } @@ -21,7 +21,7 @@ template class PerformForcedCalibrationAction : public Action class FactoryResetAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->factory_reset(); } + void play(const Ts &...x) override { this->parent_->factory_reset(); } }; } // namespace scd4x diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 870a623f32c..ec0a8f03cec 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -46,14 +46,14 @@ template class Script : public ScriptLogger, public Trigger &tuple) { - this->execute_tuple_(tuple, typename gens::type()); + this->execute_tuple_(tuple, std::make_index_sequence{}); } // Internal function to give scripts readable names. void set_name(const LogString *name) { name_ = name; } protected: - template void execute_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void execute_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->execute(std::get(tuple)...); } @@ -157,7 +157,7 @@ template class QueueingScript : public Script, public Com const size_t queue_capacity = static_cast(this->max_runs_ - 1); auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]); this->queue_front_ = (this->queue_front_ + 1) % queue_capacity; - this->trigger_tuple_(*tuple_ptr, typename gens::type()); + this->trigger_tuple_(*tuple_ptr, std::make_index_sequence{}); } } @@ -174,7 +174,7 @@ template class QueueingScript : public Script, public Com } } - template void trigger_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void trigger_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->trigger(std::get(tuple)...); } @@ -215,7 +215,7 @@ template class ScriptExecuteAction, T template void set_args(F... x) { args_ = Args{x...}; } - void play(Ts... x) override { this->script_->execute_tuple(this->eval_args_(x...)); } + void play(const Ts &...x) override { this->script_->execute_tuple(this->eval_args_(x...)); } protected: // NOTE: @@ -249,7 +249,7 @@ template class ScriptStopAction : public Action public: ScriptStopAction(C *script) : script_(script) {} - void play(Ts... x) override { this->script_->stop(); } + void play(const Ts &...x) override { this->script_->stop(); } protected: C *script_; @@ -259,7 +259,7 @@ template class IsRunningCondition : public Conditionparent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } protected: C *parent_; @@ -281,7 +281,7 @@ template class ScriptWaitAction : public Action, this->disable_loop(); } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; // Check if we can continue immediately. if (!this->script_->is_running()) { @@ -305,14 +305,14 @@ template class ScriptWaitAction : public Action, while (!this->param_queue_.empty()) { auto ¶ms = this->param_queue_.front(); - this->play_next_tuple_(params, typename gens::type()); + this->play_next_tuple_(params, std::make_index_sequence{}); this->param_queue_.pop_front(); } // Queue is now empty - disable loop until next play_complex this->disable_loop(); } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { @@ -321,7 +321,7 @@ template class ScriptWaitAction : public Action, } protected: - template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play_next_(std::get(tuple)...); } diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp index 03c2ec47458..81543055a48 100644 --- a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.cpp @@ -3,12 +3,9 @@ namespace esphome { namespace seeed_mr24hpc1 { -void ExistenceBoundarySelect::control(const std::string &value) { - this->publish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_existence_boundary(index.value()); - } +void ExistenceBoundarySelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_existence_boundary(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h index ad770a72964..933279dd13e 100644 --- a/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h +++ b/esphome/components/seeed_mr24hpc1/select/existence_boundary_select.h @@ -11,7 +11,7 @@ class ExistenceBoundarySelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_motion_boundary(index.value()); - } +void MotionBoundarySelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_motion_boundary(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h index 9058e3130b7..b0051ae6b15 100644 --- a/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h +++ b/esphome/components/seeed_mr24hpc1/select/motion_boundary_select.h @@ -11,7 +11,7 @@ class MotionBoundarySelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_scene_mode(index.value()); - } +void SceneModeSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_scene_mode(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h index 95508d49b04..f478ea5b669 100644 --- a/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h +++ b/esphome/components/seeed_mr24hpc1/select/scene_mode_select.h @@ -11,7 +11,7 @@ class SceneModeSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_unman_time(index.value()); - } +void UnmanTimeSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_unman_time(index); } } // namespace seeed_mr24hpc1 diff --git a/esphome/components/seeed_mr24hpc1/select/unman_time_select.h b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h index 7131988cdaf..a64ff4b8406 100644 --- a/esphome/components/seeed_mr24hpc1/select/unman_time_select.h +++ b/esphome/components/seeed_mr24hpc1/select/unman_time_select.h @@ -11,7 +11,7 @@ class UnmanTimeSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_height_threshold(index.value()); - } +void HeightThresholdSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_height_threshold(index); } } // namespace seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h index b856dbc89a6..f5707c7a880 100644 --- a/esphome/components/seeed_mr60fda2/select/height_threshold_select.h +++ b/esphome/components/seeed_mr60fda2/select/height_threshold_select.h @@ -11,7 +11,7 @@ class HeightThresholdSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_install_height(index.value()); - } +void InstallHeightSelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_install_height(index); } } // namespace seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/install_height_select.h b/esphome/components/seeed_mr60fda2/select/install_height_select.h index 7430da3493d..470d96c50cd 100644 --- a/esphome/components/seeed_mr60fda2/select/install_height_select.h +++ b/esphome/components/seeed_mr60fda2/select/install_height_select.h @@ -11,7 +11,7 @@ class InstallHeightSelect : public select::Select, public Parentedpublish_state(value); - auto index = this->index_of(value); - if (index.has_value()) { - this->parent_->set_sensitivity(index.value()); - } +void SensitivitySelect::control(size_t index) { + this->publish_state(index); + this->parent_->set_sensitivity(index); } } // namespace seeed_mr60fda2 diff --git a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h index d1accc1b5b6..82ed4c5d79b 100644 --- a/esphome/components/seeed_mr60fda2/select/sensitivity_select.h +++ b/esphome/components/seeed_mr60fda2/select/sensitivity_select.h @@ -11,7 +11,7 @@ class SensitivitySelect : public select::Select, public Parented class SelectSetAction : public Action { explicit SelectSetAction(Select *select) : select_(select) {} TEMPLATABLE_VALUE(std::string, option) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->select_->make_call(); call.set_option(this->option_.value(x...)); call.perform(); @@ -34,7 +34,7 @@ template class SelectSetIndexAction : public Action { explicit SelectSetIndexAction(Select *select) : select_(select) {} TEMPLATABLE_VALUE(size_t, index) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->select_->make_call(); call.set_index(this->index_.value(x...)); call.perform(); @@ -50,7 +50,7 @@ template class SelectOperationAction : public Action { TEMPLATABLE_VALUE(bool, cycle) TEMPLATABLE_VALUE(SelectOperation, operation) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->select_->make_call(); call.with_operation(this->operation_.value(x...)); if (this->cycle_.has_value()) { diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index fae485709ed..9fe7a524227 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -1,4 +1,6 @@ #include "select.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include @@ -33,9 +35,12 @@ void Select::publish_state(size_t index) { ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); // Callback signature requires std::string, create temporary for compatibility this->state_callback_.call(std::string(option), index); +#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_select_update(this); +#endif } -const char *Select::current_option() const { return this->option_at(this->active_index_); } +const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; } void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 030646c1ad6..f72bdfb675b 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -35,7 +35,7 @@ class Select : public EntityBase { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0. - __attribute__((deprecated("Use current_option() instead of .state. Will be removed in 2026.5.0"))) + ESPDEPRECATED("Use current_option() instead of .state. Will be removed in 2026.5.0", "2025.11.0") std::string state{}; Select() = default; @@ -94,18 +94,13 @@ class Select : public EntityBase { /** Set the value of the select, this is a virtual method that each select integration can implement. * - * This method is called by control(size_t) when not overridden, or directly by external code. - * Integrations can either: - * 1. Override this method to handle string-based control (traditional approach) - * 2. Override control(size_t) instead to work with indices directly (recommended) + * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes. + * Overriding control(size_t) is PREFERRED as it avoids string conversions. * + * This method is called by control(size_t) when not overridden, or directly by external code. * Default implementation converts to index and calls control(size_t). * - * Delegation chain: - * - SelectCall::perform() → control(size_t) → [if not overridden] → control(string) - * - External code → control(string) → publish_state(string) → publish_state(size_t) - * - * @param value The value as validated by the SelectCall. + * @param value The value as validated by the caller. */ virtual void control(const std::string &value) { auto index = this->index_of(value); diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp index c6ded98ebf7..e5e12bdc7a4 100644 --- a/esphome/components/select/select_traits.cpp +++ b/esphome/components/select/select_traits.cpp @@ -7,8 +7,8 @@ void SelectTraits::set_options(const std::initializer_list &option void SelectTraits::set_options(const FixedVector &options) { this->options_.init(options.size()); - for (size_t i = 0; i < options.size(); i++) { - this->options_[i] = options[i]; + for (const auto &opt : options) { + this->options_.push_back(opt); } } diff --git a/esphome/components/sen5x/automation.h b/esphome/components/sen5x/automation.h index 423b9420005..558ea46e47f 100644 --- a/esphome/components/sen5x/automation.h +++ b/esphome/components/sen5x/automation.h @@ -11,7 +11,7 @@ template class StartFanAction : public Action { public: explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {} - void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); } + void play(const Ts &...x) override { this->sen5x_->start_fan_cleaning(); } protected: SEN5XComponent *sen5x_; diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index 5b66860f1a4..9db849075d9 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -42,7 +42,7 @@ template class SenseAirBackgroundCalibrationAction : public Acti public: SenseAirBackgroundCalibrationAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->background_calibration(); } + void play(const Ts &...x) override { this->senseair_->background_calibration(); } protected: SenseAirComponent *senseair_; @@ -52,7 +52,7 @@ template class SenseAirBackgroundCalibrationResultAction : publi public: SenseAirBackgroundCalibrationResultAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->background_calibration_result(); } + void play(const Ts &...x) override { this->senseair_->background_calibration_result(); } protected: SenseAirComponent *senseair_; @@ -62,7 +62,7 @@ template class SenseAirABCEnableAction : public Action { public: SenseAirABCEnableAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->abc_enable(); } + void play(const Ts &...x) override { this->senseair_->abc_enable(); } protected: SenseAirComponent *senseair_; @@ -72,7 +72,7 @@ template class SenseAirABCDisableAction : public Action { public: SenseAirABCDisableAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->abc_disable(); } + void play(const Ts &...x) override { this->senseair_->abc_disable(); } protected: SenseAirComponent *senseair_; @@ -82,7 +82,7 @@ template class SenseAirABCGetPeriodAction : public Action public: SenseAirABCGetPeriodAction(SenseAirComponent *senseair) : senseair_(senseair) {} - void play(Ts... x) override { this->senseair_->abc_get_period(); } + void play(const Ts &...x) override { this->senseair_->abc_get_period(); } protected: SenseAirComponent *senseair_; diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 4f34c35023a..df7d31a0c92 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -26,7 +26,7 @@ template class SensorPublishAction : public Action { SensorPublishAction(Sensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(float, state) - void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: Sensor *sensor_; @@ -90,7 +90,7 @@ template class SensorInRangeCondition : public Condition void set_min(float min) { this->min_ = min; } void set_max(float max) { this->max_ = max; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { const float state = this->parent_->state; if (std::isnan(this->min_)) { return state <= this->max_; diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 92da4345b70..30187f498b0 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -1,4 +1,6 @@ #include "sensor.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -72,9 +74,9 @@ StateClass Sensor::get_state_class() { void Sensor::publish_state(float state) { this->raw_state = state; - if (this->raw_callback_) { - this->raw_callback_->call(state); - } + + // Call raw callbacks (before filters) + this->callbacks_.call_first(this->raw_count_, state); ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); @@ -85,12 +87,12 @@ void Sensor::publish_state(float state) { } } -void Sensor::add_on_state_callback(std::function &&callback) { this->callback_.add(std::move(callback)); } +void Sensor::add_on_state_callback(std::function &&callback) { + this->callbacks_.add_second(std::move(callback)); +} + void Sensor::add_on_raw_state_callback(std::function &&callback) { - if (!this->raw_callback_) { - this->raw_callback_ = make_unique>(); - } - this->raw_callback_->add(std::move(callback)); + this->callbacks_.add_first(std::move(callback), &this->raw_count_); } void Sensor::add_filter(Filter *filter) { @@ -130,7 +132,13 @@ void Sensor::internal_send_state_to_frontend(float state) { this->state = state; ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); - this->callback_.call(state); + + // Call filtered callbacks (after filters) + this->callbacks_.call_second(this->raw_count_, state); + +#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_sensor_update(this); +#endif } } // namespace sensor diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index a4210e5e6c3..42e540a3491 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -124,8 +124,7 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa void internal_send_state_to_frontend(float state); protected: - std::unique_ptr> raw_callback_; ///< Storage for raw state callbacks (lazy allocated). - CallbackManager callback_; ///< Storage for filtered state callbacks. + PartitionedCallbackManager callbacks_; Filter *filter_list_{nullptr}; ///< Store all active filters. @@ -140,6 +139,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa uint8_t force_update : 1; uint8_t reserved : 5; // Reserved for future use } sensor_flags_{}; + + uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector) }; } // namespace sensor diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index ff1708dc534..3d15aefefec 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -57,7 +57,7 @@ template class ServoWriteAction : public Action { ServoWriteAction(Servo *servo) : servo_(servo) {} TEMPLATABLE_VALUE(float, value) - void play(Ts... x) override { this->servo_->write(this->value_.value(x...)); } + void play(const Ts &...x) override { this->servo_->write(this->value_.value(x...)); } protected: Servo *servo_; @@ -67,7 +67,7 @@ template class ServoDetachAction : public Action { public: ServoDetachAction(Servo *servo) : servo_(servo) {} - void play(Ts... x) override { this->servo_->detach(); } + void play(const Ts &...x) override { this->servo_->detach(); } protected: Servo *servo_; diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 0e3aeff8129..9e8d6b332c9 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -78,7 +78,7 @@ void SGP30Component::setup() { uint32_t hash = fnv1_hash(App.get_compilation_time() + std::to_string(this->serial_number_)); this->pref_ = global_preferences->make_preference(hash, true); - if (this->pref_.load(&this->baselines_storage_)) { + if (this->store_baseline_ && this->pref_.load(&this->baselines_storage_)) { ESP_LOGI(TAG, "Loaded eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, baselines_storage_.tvoc); this->eco2_baseline_ = this->baselines_storage_.eco2; diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index bf7efd69154..a2da686ce1f 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -162,7 +162,7 @@ template class Sim800LSendSmsAction : public Action { TEMPLATABLE_VALUE(std::string, recipient) TEMPLATABLE_VALUE(std::string, message) - void play(Ts... x) { + void play(const Ts &...x) { auto recipient = this->recipient_.value(x...); auto message = this->message_.value(x...); this->parent_->send_sms(recipient, message); @@ -177,7 +177,7 @@ template class Sim800LSendUssdAction : public Action { Sim800LSendUssdAction(Sim800LComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, ussd) - void play(Ts... x) { + void play(const Ts &...x) { auto ussd_code = this->ussd_.value(x...); this->parent_->send_ussd(ussd_code); } @@ -191,7 +191,7 @@ template class Sim800LDialAction : public Action { Sim800LDialAction(Sim800LComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(std::string, recipient) - void play(Ts... x) { + void play(const Ts &...x) { auto recipient = this->recipient_.value(x...); this->parent_->dial(recipient); } @@ -203,7 +203,7 @@ template class Sim800LConnectAction : public Action { public: Sim800LConnectAction(Sim800LComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->connect(); } + void play(const Ts &...x) { this->parent_->connect(); } protected: Sim800LComponent *parent_; @@ -213,7 +213,7 @@ template class Sim800LDisconnectAction : public Action { public: Sim800LDisconnectAction(Sim800LComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->disconnect(); } + void play(const Ts &...x) { this->parent_->disconnect(); } protected: Sim800LComponent *parent_; diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 331a9b35099..c4d78b6e0b1 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -61,6 +61,7 @@ void SNTPComponent::dump_config() { for (auto &server : this->servers_) { ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server); } + RealTimeClock::dump_config(); } void SNTPComponent::update() { #if !defined(USE_ESP32) diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index d27fc9991de..69a2436d3d2 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,9 +1,14 @@ +import logging + import esphome.codegen as cg from esphome.components import time as time_ +from esphome.config_helpers import merge_config import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_PLATFORM, CONF_SERVERS, + CONF_TIME, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -12,13 +17,74 @@ from esphome.const import ( PLATFORM_RTL87XX, ) from esphome.core import CORE +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) DEPENDENCIES = ["network"] + +CONF_SNTP = "sntp" + sntp_ns = cg.esphome_ns.namespace("sntp") SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock) DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"] + +def _sntp_final_validate(config: ConfigType) -> None: + """Merge multiple SNTP instances into one, similar to OTA merging behavior.""" + full_conf = fv.full_config.get() + time_confs = full_conf.get(CONF_TIME, []) + + sntp_configs: list[ConfigType] = [] + other_time_configs: list[ConfigType] = [] + + for time_conf in time_confs: + if time_conf.get(CONF_PLATFORM) == CONF_SNTP: + sntp_configs.append(time_conf) + else: + other_time_configs.append(time_conf) + + if len(sntp_configs) <= 1: + return + + # Merge all SNTP configs into the first one + merged = sntp_configs[0] + for sntp_conf in sntp_configs[1:]: + # Validate that IDs are consistent if manually specified + if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual: + raise cv.Invalid( + f"Found multiple SNTP configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, sntp_conf) + + # Deduplicate servers while preserving order + servers = merged[CONF_SERVERS] + unique_servers = list(dict.fromkeys(servers)) + + # Warn if we're dropping servers due to 3-server limit + if len(unique_servers) > 3: + dropped = unique_servers[3:] + unique_servers = unique_servers[:3] + _LOGGER.warning( + "SNTP supports maximum 3 servers. Dropped excess server(s): %s", + dropped, + ) + + merged[CONF_SERVERS] = unique_servers + + _LOGGER.warning( + "Found and merged %d SNTP time configurations into one instance", + len(sntp_configs), + ) + + # Replace time configs with merged SNTP + other time platforms + other_time_configs.append(merged) + full_conf[CONF_TIME] = other_time_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = cv.All( time_.TIME_SCHEMA.extend( { @@ -40,6 +106,8 @@ CONFIG_SCHEMA = cv.All( ), ) +FINAL_VALIDATE_SCHEMA = _sntp_final_validate + async def to_code(config): servers = config[CONF_SERVERS] diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index e6a4cfc07ff..49e074a6ee3 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -15,6 +15,9 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" # Components register their socket needs and platforms read this to configure appropriately KEY_SOCKET_CONSUMERS = "socket_consumers" +# Wake loop threadsafe support tracking +KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required" + def consume_sockets( value: int, consumer: str @@ -37,6 +40,30 @@ def consume_sockets( return _consume_sockets +def require_wake_loop_threadsafe() -> None: + """Mark that wake_loop_threadsafe support is required by a component. + + 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. + + IMPORTANT: This is for background thread context only, NOT ISR context. + Socket operations are not safe to call from ISR handlers. + + Example: + from esphome.components import socket + + async def to_code(config): + socket.require_wake_loop_threadsafe() + """ + # Only set up once (idempotent - multiple components can call this) + if 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") + # Consume 1 socket for the shared wake notification socket + consume_sockets(1, "socket.wake_loop_threadsafe")({}) + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 4dedeffb6a2..e0d93d8e2f1 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -172,16 +172,7 @@ class LWIPRawImpl : public Socket { errno = ECONNRESET; return ""; } - char buffer[50] = {}; - if (IP_IS_V4_VAL(pcb_->remote_ip)) { - inet_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(pcb_->remote_ip)) { - inet6_ntoa_r(pcb_->remote_ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); + return this->format_ip_address_(pcb_->remote_ip); } int getsockname(struct sockaddr *name, socklen_t *addrlen) override { if (pcb_ == nullptr) { @@ -199,16 +190,7 @@ class LWIPRawImpl : public Socket { errno = ECONNRESET; return ""; } - char buffer[50] = {}; - if (IP_IS_V4_VAL(pcb_->local_ip)) { - inet_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer)); - } -#if LWIP_IPV6 - else if (IP_IS_V6_VAL(pcb_->local_ip)) { - inet6_ntoa_r(pcb_->local_ip, buffer, sizeof(buffer)); - } -#endif - return std::string(buffer); + return this->format_ip_address_(pcb_->local_ip); } int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override { if (pcb_ == nullptr) { @@ -499,6 +481,19 @@ class LWIPRawImpl : public Socket { } protected: + std::string format_ip_address_(const ip_addr_t &ip) { + char buffer[50] = {}; + if (IP_IS_V4_VAL(ip)) { + inet_ntoa_r(ip, buffer, sizeof(buffer)); + } +#if LWIP_IPV6 + else if (IP_IS_V6_VAL(ip)) { + inet6_ntoa_r(ip, buffer, sizeof(buffer)); + } +#endif + return std::string(buffer); + } + int ip2sockaddr_(ip_addr_t *ip, uint16_t port, struct sockaddr *name, socklen_t *addrlen) { if (family_ == AF_INET) { if (*addrlen < sizeof(struct sockaddr_in)) { diff --git a/esphome/components/sound_level/sound_level.h b/esphome/components/sound_level/sound_level.h index 6a80a60ac71..dc35f69fe25 100644 --- a/esphome/components/sound_level/sound_level.h +++ b/esphome/components/sound_level/sound_level.h @@ -60,12 +60,12 @@ class SoundLevelComponent : public Component { template class StartAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->start(); } + void play(const Ts &...x) override { this->parent_->start(); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; } // namespace sound_level diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 5f1ba94ee60..18e1d9782cc 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components import audio, audio_dac import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME -from esphome.core import CORE +from esphome.core import CORE, ID from esphome.coroutine import CoroPriority, coroutine_with_priority AUTO_LOAD = ["audio"] @@ -90,7 +90,10 @@ async def speaker_play_action(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index c083796eea9..391c9e4c627 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -10,40 +10,45 @@ namespace speaker { template class PlayAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; - } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->play(this->data_static_); + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void play(const Ts &...x) override { + if (this->len_ >= 0) { + // Static mode: pass pointer directly to play(const uint8_t *, size_t) + this->parent_->play(this->data_.data, static_cast(this->len_)); } else { - auto val = this->data_func_(x...); + // Template mode: call function and pass vector to play(const std::vector &) + auto val = this->data_.func(x...); this->parent_->play(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; template class VolumeSetAction : public Action, public Parented { TEMPLATABLE_VALUE(float, volume) - void play(Ts... x) override { this->parent_->set_volume(this->volume_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_volume(this->volume_.value(x...)); } }; template class MuteOnAction : public Action { public: explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {} - void play(Ts... x) override { this->speaker_->set_mute_state(true); } + void play(const Ts &...x) override { this->speaker_->set_mute_state(true); } protected: Speaker *speaker_; @@ -53,7 +58,7 @@ template class MuteOffAction : public Action { public: explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {} - void play(Ts... x) override { this->speaker_->set_mute_state(false); } + void play(const Ts &...x) override { this->speaker_->set_mute_state(false); } protected: Speaker *speaker_; @@ -61,22 +66,22 @@ template class MuteOffAction : public Action { template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->stop(); } + void play(const Ts &...x) override { this->parent_->stop(); } }; template class FinishAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->finish(); } + void play(const Ts &...x) override { this->parent_->finish(); } }; template class IsPlayingCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running(); } + bool check(const Ts &...x) override { return this->parent_->is_running(); } }; template class IsStoppedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_stopped(); } + bool check(const Ts &...x) override { return this->parent_->is_stopped(); } }; } // namespace speaker diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 7537a61e4ea..062bff92f8b 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -6,7 +6,7 @@ from pathlib import Path from esphome import automation, external_files import esphome.codegen as cg -from esphome.components import audio, esp32, media_player, psram, speaker +from esphome.components import audio, esp32, media_player, network, psram, speaker import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -26,21 +26,13 @@ from esphome.const import ( from esphome.core import CORE, HexInt from esphome.core.entity_helpers import inherit_property_from from esphome.external_files import download_content -from esphome.types import ConfigType +from esphome.final_validate import full_config _LOGGER = logging.getLogger(__name__) -def AUTO_LOAD(config: ConfigType) -> list[str]: - load = ["audio"] - if ( - not config - or config.get(CONF_TASK_STACK_IN_PSRAM) - or config.get(CONF_CODEC_SUPPORT_ENABLED) - ): - return load + ["psram"] - return load - +AUTO_LOAD = ["audio"] +DEPENDENCIES = ["network"] CODEOWNERS = ["@kahrendt", "@synesthesiam"] DOMAIN = "media_player" @@ -226,12 +218,19 @@ def _validate_repeated_speaker(config): return config -def _validate_supported_local_file(config): +def _final_validate(config): + # Default to using codec if psram is enabled + if (use_codec := config.get(CONF_CODEC_SUPPORT_ENABLED)) is None: + use_codec = psram.DOMAIN in full_config.get() + conf_id = config[CONF_ID].id + core_data = CORE.data.setdefault(DOMAIN, {conf_id: {}}) + core_data[conf_id][CONF_CODEC_SUPPORT_ENABLED] = use_codec + for file_config in config.get(CONF_FILES, []): _, media_file_type = _read_audio_file_and_type(file_config) if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): raise cv.Invalid("Unsupported local media file") - if not config[CONF_CODEC_SUPPORT_ENABLED] and str(media_file_type) != str( + if not use_codec and str(media_file_type) != str( audio.AUDIO_FILE_TYPE_ENUM["WAV"] ): # Only wav files are supported @@ -282,6 +281,18 @@ PIPELINE_SCHEMA = cv.Schema( } ) + +def _request_high_performance_networking(config): + """Request high performance networking for streaming media. + + Speaker media player streams audio data, so it always benefits from + optimized WiFi and lwip settings regardless of codec support. + Called during config validation to ensure flags are set before to_code(). + """ + network.require_high_performance_networking() + return config + + CONFIG_SCHEMA = cv.All( media_player.media_player_schema(SpeakerMediaPlayer).extend( { @@ -290,11 +301,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - cv.Optional( - CONF_CODEC_SUPPORT_ENABLED, default=psram.supported() - ): cv.boolean, + cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.boolean, cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA), - cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean, + cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All( + cv.boolean, cv.requires_component(psram.DOMAIN) + ), cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, cv.Optional(CONF_VOLUME_INITIAL, default=0.5): cv.percentage, cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage, @@ -306,6 +317,7 @@ CONFIG_SCHEMA = cv.All( ), cv.only_with_esp_idf, _validate_repeated_speaker, + _request_high_performance_networking, ) @@ -317,34 +329,16 @@ FINAL_VALIDATE_SCHEMA = cv.All( }, extra=cv.ALLOW_EXTRA, ), - _validate_supported_local_file, + _final_validate, ) async def to_code(config): - if config[CONF_CODEC_SUPPORT_ENABLED]: - # Compile all supported audio codecs and optimize the wifi settings - + if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]: + # Compile all supported audio codecs cg.add_define("USE_AUDIO_FLAC_SUPPORT", True) cg.add_define("USE_AUDIO_MP3_SUPPORT", True) - # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) - esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) - - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64) - esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64) - - # Allocate wifi buffers in PSRAM - esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) - var = await media_player.new_media_player(config) await cg.register_component(var, config) @@ -352,8 +346,8 @@ async def to_code(config): cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) - cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM])) - if config[CONF_TASK_STACK_IN_PSRAM]: + if config.get(CONF_TASK_STACK_IN_PSRAM): + cg.add(var.set_task_stack_in_psram(True)) esp32.add_idf_sdkconfig_option( "CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True ) diff --git a/esphome/components/speaker/media_player/automation.h b/esphome/components/speaker/media_player/automation.h index d1a01aabc48..fdf3db07f9f 100644 --- a/esphome/components/speaker/media_player/automation.h +++ b/esphome/components/speaker/media_player/automation.h @@ -14,7 +14,7 @@ template class PlayOnDeviceMediaAction : public Action, p TEMPLATABLE_VALUE(audio::AudioFile *, audio_file) TEMPLATABLE_VALUE(bool, announcement) TEMPLATABLE_VALUE(bool, enqueue) - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->play_file(this->audio_file_.value(x...), this->announcement_.value(x...), this->enqueue_.value(x...)); } diff --git a/esphome/components/split_buffer/split_buffer.cpp b/esphome/components/split_buffer/split_buffer.cpp index a710670a5dd..526a19c71cc 100644 --- a/esphome/components/split_buffer/split_buffer.cpp +++ b/esphome/components/split_buffer/split_buffer.cpp @@ -4,7 +4,6 @@ #include "esphome/core/log.h" namespace esphome::split_buffer { - static constexpr const char *const TAG = "split_buffer"; SplitBuffer::~SplitBuffer() { this->free(); } @@ -102,32 +101,44 @@ void SplitBuffer::free() { this->total_length_ = 0; } -uint8_t &SplitBuffer::operator[](size_t index) { +const uint8_t &SplitBuffer::operator[](size_t index) const { if (index >= this->total_length_) { ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); - // Return reference to a static dummy byte to avoid crash + // Return reference to a static dummy byte since we can't throw exceptions. + // the byte is non-const since it will also be used by the non-const [] overload. static uint8_t dummy = 0; return dummy; } - size_t buffer_index = index / this->buffer_size_; - size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; + const auto buffer_index = index / this->buffer_size_; + const auto offset_in_buffer = index % this->buffer_size_; return this->buffers_[buffer_index][offset_in_buffer]; } -const uint8_t &SplitBuffer::operator[](size_t index) const { - if (index >= this->total_length_) { - ESP_LOGE(TAG, "Out of bounds - %zu >= %zu", index, this->total_length_); - // Return reference to a static dummy byte to avoid crash - static const uint8_t DUMMY = 0; - return DUMMY; +// non-const version of operator[] for write access +uint8_t &SplitBuffer::operator[](size_t index) { + // avoid code duplication. These casts are safe since we know the object is not const. + return const_cast(static_cast(this)->operator[](index)); +} + +/** + * Fill the entire buffer with a single byte value + * @param value Fill value + */ +void SplitBuffer::fill(uint8_t value) const { + if (this->buffer_count_ == 0) + return; + // clear all the full sized buffers + size_t i = 0; + for (; i != this->buffer_count_ - 1; i++) { + memset(this->buffers_[i], value, this->buffer_size_); } - - size_t buffer_index = index / this->buffer_size_; - size_t offset_in_buffer = index - this->buffer_size_ * buffer_index; - - return this->buffers_[buffer_index][offset_in_buffer]; + // clear the last, potentially short, buffer. + // `i` is guaranteed to equal the last index since the loop terminates at that value. + // where all buffers are the same size, the modulus must return the size, not 0. + auto size_last = ((this->total_length_ - 1) % this->buffer_size_) + 1; + memset(this->buffers_[i], value, size_last); } } // namespace esphome::split_buffer diff --git a/esphome/components/split_buffer/split_buffer.h b/esphome/components/split_buffer/split_buffer.h index c3490f3d6eb..b615ddce74d 100644 --- a/esphome/components/split_buffer/split_buffer.h +++ b/esphome/components/split_buffer/split_buffer.h @@ -4,7 +4,13 @@ #include namespace esphome::split_buffer { - +/** + * A SplitBuffer allocates a large memory buffer potentially as multiple smaller buffers + * to facilitate allocation of large buffers on devices with fragmented memory spaces. + * Each sub-buffer is the same size, except for the last one which may be smaller. + * Standard array indexing using `[]` is possible on the buffer, but, since the buffer may not be contiguous in memory, + * there is no easy way to access the buffer as a single array, i.e. no `.data()` access like a vector. + */ class SplitBuffer { public: SplitBuffer() = default; @@ -19,13 +25,13 @@ class SplitBuffer { // Access operators uint8_t &operator[](size_t index); const uint8_t &operator[](size_t index) const; + void fill(uint8_t value) const; // Get the total length size_t size() const { return this->total_length_; } // Get buffer information size_t get_buffer_count() const { return this->buffer_count_; } - size_t get_buffer_size() const { return this->buffer_size_; } // Check if successfully initialized bool is_valid() const { return this->buffers_ != nullptr && this->buffer_count_ > 0; } diff --git a/esphome/components/sprinkler/automation.h b/esphome/components/sprinkler/automation.h index 59c6cd50e1f..d6c877ae903 100644 --- a/esphome/components/sprinkler/automation.h +++ b/esphome/components/sprinkler/automation.h @@ -13,7 +13,7 @@ template class SetDividerAction : public Action { TEMPLATABLE_VALUE(uint32_t, divider) - void play(Ts... x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); } + void play(const Ts &...x) override { this->sprinkler_->set_divider(this->divider_.optional_value(x...)); } protected: Sprinkler *sprinkler_; @@ -25,7 +25,7 @@ template class SetMultiplierAction : public Action { TEMPLATABLE_VALUE(float, multiplier) - void play(Ts... x) override { this->sprinkler_->set_multiplier(this->multiplier_.optional_value(x...)); } + void play(const Ts &...x) override { this->sprinkler_->set_multiplier(this->multiplier_.optional_value(x...)); } protected: Sprinkler *sprinkler_; @@ -38,7 +38,7 @@ template class QueueValveAction : public Action { TEMPLATABLE_VALUE(size_t, valve_number) TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->sprinkler_->queue_valve(this->valve_number_.optional_value(x...), this->valve_run_duration_.optional_value(x...)); } @@ -51,7 +51,7 @@ template class ClearQueuedValvesAction : public Action { public: explicit ClearQueuedValvesAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->clear_queued_valves(); } + void play(const Ts &...x) override { this->sprinkler_->clear_queued_valves(); } protected: Sprinkler *sprinkler_; @@ -63,7 +63,7 @@ template class SetRepeatAction : public Action { TEMPLATABLE_VALUE(uint32_t, repeat) - void play(Ts... x) override { this->sprinkler_->set_repeat(this->repeat_.optional_value(x...)); } + void play(const Ts &...x) override { this->sprinkler_->set_repeat(this->repeat_.optional_value(x...)); } protected: Sprinkler *sprinkler_; @@ -76,7 +76,7 @@ template class SetRunDurationAction : public Action { TEMPLATABLE_VALUE(size_t, valve_number) TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->sprinkler_->set_valve_run_duration(this->valve_number_.optional_value(x...), this->valve_run_duration_.optional_value(x...)); } @@ -89,7 +89,7 @@ template class StartFromQueueAction : public Action { public: explicit StartFromQueueAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->start_from_queue(); } + void play(const Ts &...x) override { this->sprinkler_->start_from_queue(); } protected: Sprinkler *sprinkler_; @@ -99,7 +99,7 @@ template class StartFullCycleAction : public Action { public: explicit StartFullCycleAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->start_full_cycle(); } + void play(const Ts &...x) override { this->sprinkler_->start_full_cycle(); } protected: Sprinkler *sprinkler_; @@ -112,7 +112,7 @@ template class StartSingleValveAction : public Action { TEMPLATABLE_VALUE(size_t, valve_to_start) TEMPLATABLE_VALUE(uint32_t, valve_run_duration) - void play(Ts... x) override { + void play(const Ts &...x) override { this->sprinkler_->start_single_valve(this->valve_to_start_.optional_value(x...), this->valve_run_duration_.optional_value(x...)); } @@ -125,7 +125,7 @@ template class ShutdownAction : public Action { public: explicit ShutdownAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->shutdown(); } + void play(const Ts &...x) override { this->sprinkler_->shutdown(); } protected: Sprinkler *sprinkler_; @@ -135,7 +135,7 @@ template class NextValveAction : public Action { public: explicit NextValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->next_valve(); } + void play(const Ts &...x) override { this->sprinkler_->next_valve(); } protected: Sprinkler *sprinkler_; @@ -145,7 +145,7 @@ template class PreviousValveAction : public Action { public: explicit PreviousValveAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->previous_valve(); } + void play(const Ts &...x) override { this->sprinkler_->previous_valve(); } protected: Sprinkler *sprinkler_; @@ -155,7 +155,7 @@ template class PauseAction : public Action { public: explicit PauseAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->pause(); } + void play(const Ts &...x) override { this->sprinkler_->pause(); } protected: Sprinkler *sprinkler_; @@ -165,7 +165,7 @@ template class ResumeAction : public Action { public: explicit ResumeAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->resume(); } + void play(const Ts &...x) override { this->sprinkler_->resume(); } protected: Sprinkler *sprinkler_; @@ -175,7 +175,7 @@ template class ResumeOrStartAction : public Action { public: explicit ResumeOrStartAction(Sprinkler *a_sprinkler) : sprinkler_(a_sprinkler) {} - void play(Ts... x) override { this->sprinkler_->resume_or_start_full_cycle(); } + void play(const Ts &...x) override { this->sprinkler_->resume_or_start_full_cycle(); } protected: Sprinkler *sprinkler_; diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 7676e174688..8edb240a415 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -650,7 +650,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -732,7 +732,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); diff --git a/esphome/components/sps30/automation.h b/esphome/components/sps30/automation.h index 443aafb5756..67af8136872 100644 --- a/esphome/components/sps30/automation.h +++ b/esphome/components/sps30/automation.h @@ -11,7 +11,7 @@ template class StartFanAction : public Action { public: explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {} - void play(Ts... x) override { this->sps30_->start_fan_cleaning(); } + void play(const Ts &...x) override { this->sps30_->start_fan_cleaning(); } protected: SPS30Component *sps30_; diff --git a/esphome/components/st7920/st7920.h b/esphome/components/st7920/st7920.h index c9fdad454d3..c48fe8cc1c3 100644 --- a/esphome/components/st7920/st7920.h +++ b/esphome/components/st7920/st7920.h @@ -9,7 +9,7 @@ namespace st7920 { class ST7920; -using st7920_writer_t = std::function; +using st7920_writer_t = display::DisplayWriter; class ST7920 : public display::DisplayBuffer, public spi::SPIDevice writer_local_{}; + st7920_writer_t writer_local_{}; }; } // namespace st7920 diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 1cf4830b1f1..2bad672494f 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -44,7 +44,7 @@ template class SetTargetAction : public Action { TEMPLATABLE_VALUE(int32_t, target) - void play(Ts... x) override { this->parent_->set_target(this->target_.value(x...)); } + void play(const Ts &...x) override { this->parent_->set_target(this->target_.value(x...)); } protected: Stepper *parent_; @@ -56,7 +56,7 @@ template class ReportPositionAction : public Action { TEMPLATABLE_VALUE(int32_t, position) - void play(Ts... x) override { this->parent_->report_position(this->position_.value(x...)); } + void play(const Ts &...x) override { this->parent_->report_position(this->position_.value(x...)); } protected: Stepper *parent_; @@ -68,7 +68,7 @@ template class SetSpeedAction : public Action { TEMPLATABLE_VALUE(float, speed); - void play(Ts... x) override { + void play(const Ts &...x) override { float speed = this->speed_.value(x...); this->parent_->set_max_speed(speed); this->parent_->on_update_speed(); @@ -84,7 +84,7 @@ template class SetAccelerationAction : public Action { TEMPLATABLE_VALUE(float, acceleration); - void play(Ts... x) override { + void play(const Ts &...x) override { float acceleration = this->acceleration_.value(x...); this->parent_->set_acceleration(acceleration); } @@ -99,7 +99,7 @@ template class SetDecelerationAction : public Action { TEMPLATABLE_VALUE(float, deceleration); - void play(Ts... x) override { + void play(const Ts &...x) override { float deceleration = this->deceleration_.value(x...); this->parent_->set_deceleration(deceleration); } diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 77d62d34c38..67a0306a37b 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -115,7 +115,7 @@ template class SunCondition : public Condition, public Pa TEMPLATABLE_VALUE(double, elevation); void set_above(bool above) { above_ = above; } - bool check(Ts... x) override { + bool check(const Ts &...x) override { double elevation = this->elevation_.value(x...); double current = this->parent_->elevation(); if (this->above_) { diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index b8cbc9b9769..27d3474c971 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -11,7 +11,7 @@ template class TurnOnAction : public Action { public: explicit TurnOnAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->turn_on(); } + void play(const Ts &...x) override { this->switch_->turn_on(); } protected: Switch *switch_; @@ -21,7 +21,7 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->turn_off(); } + void play(const Ts &...x) override { this->switch_->turn_off(); } protected: Switch *switch_; @@ -31,7 +31,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->toggle(); } + void play(const Ts &...x) override { this->switch_->toggle(); } protected: Switch *switch_; @@ -43,7 +43,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + void play(const Ts &...x) override { auto state = this->state_.optional_value(x...); if (state.has_value()) { this->switch_->control(*state); @@ -57,7 +57,7 @@ template class ControlAction : public Action { template class SwitchCondition : public Condition { public: SwitchCondition(Switch *parent, bool state) : parent_(parent), state_(state) {} - bool check(Ts... x) override { return this->parent_->state == this->state_; } + bool check(const Ts &...x) override { return this->parent_->state == this->state_; } protected: Switch *parent_; @@ -98,7 +98,7 @@ template class SwitchPublishAction : public Action { SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->switch_->publish_state(this->state_.value(x...)); } protected: Switch *switch_; diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 02cee91a768..3c3a437ff36 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -1,4 +1,6 @@ #include "switch.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -62,6 +64,9 @@ void Switch::publish_state(bool state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state)); this->state_callback_.call(this->state); +#if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_switch_update(this); +#endif } bool Switch::assumed_state() { return false; } diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index 370cd102d49..1eb83b7a334 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components import spi import esphome.config_validation as cv from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID -from esphome.core import TimePeriod +from esphome.core import ID, TimePeriod MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -189,7 +189,7 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(SX126x), cv.Optional(CONF_BANDWIDTH, default="125_0kHz"): cv.enum(BW), cv.Optional(CONF_BITRATE, default=4800): cv.int_range(min=600, max=300000), - cv.Required(CONF_BUSY_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_BUSY_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE), cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean, cv.Optional(CONF_CRC_INVERTED, default=True): cv.boolean, @@ -201,7 +201,7 @@ CONFIG_SCHEMA = ( cv.hex_int, cv.Range(min=0, max=0xFFFF) ), cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000), - cv.Required(CONF_DIO1_PIN): pins.internal_gpio_input_pin_schema, + cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema, cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000), cv.Required(CONF_HW_VERSION): cv.one_of( "sx1261", "sx1262", "sx1268", "llcc68", lower=True @@ -213,7 +213,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4), cv.Optional(CONF_PREAMBLE_SIZE, default=8): cv.int_range(min=1, max=65535), - cv.Required(CONF_RST_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_RST_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RX_START, default=True): cv.boolean, cv.Required(CONF_RF_SWITCH): cv.boolean, cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING), @@ -329,5 +329,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/sx126x/automation.h b/esphome/components/sx126x/automation.h index 520ef997188..2282c583cbf 100644 --- a/esphome/components/sx126x/automation.h +++ b/esphome/components/sx126x/automation.h @@ -9,53 +9,59 @@ namespace sx126x { template class RunImageCalAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->run_image_cal(); } + void play(const Ts &...x) override { this->parent_->run_image_cal(); } }; template class SendPacketAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->transmit_packet(this->data_static_); + void play(const Ts &...x) override { + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { - this->parent_->transmit_packet(this->data_func_(x...)); + // Template mode: call function + data = this->data_.func(x...); } + this->parent_->transmit_packet(data); } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; template class SetModeTxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_tx(); } + void play(const Ts &...x) override { this->parent_->set_mode_tx(); } }; template class SetModeRxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_rx(); } + void play(const Ts &...x) override { this->parent_->set_mode_rx(); } }; template class SetModeSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_sleep(); } + void play(const Ts &...x) override { this->parent_->set_mode_sleep(); } }; template class SetModeStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_standby(STDBY_XOSC); } + void play(const Ts &...x) override { this->parent_->set_mode_standby(STDBY_XOSC); } }; } // namespace sx126x diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index 47d6449738a..850d7d4c77d 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -64,7 +64,7 @@ class SX126x : public Component, void dump_config() override; void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; } void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; } - void set_busy_pin(InternalGPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } + void set_busy_pin(GPIOPin *busy_pin) { this->busy_pin_ = busy_pin; } void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; } void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; } void set_crc_inverted(bool crc_inverted) { this->crc_inverted_ = crc_inverted; } @@ -72,7 +72,7 @@ class SX126x : public Component, void set_crc_polynomial(uint16_t crc_polynomial) { this->crc_polynomial_ = crc_polynomial; } void set_crc_initial(uint16_t crc_initial) { this->crc_initial_ = crc_initial; } void set_deviation(uint32_t deviation) { this->deviation_ = deviation; } - void set_dio1_pin(InternalGPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } + void set_dio1_pin(GPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; } void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } void set_hw_version(const std::string &hw_version) { this->hw_version_ = hw_version; } void set_mode_rx(); @@ -85,7 +85,7 @@ class SX126x : public Component, void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; } void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; } void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; } - void set_rst_pin(InternalGPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } + void set_rst_pin(GPIOPin *rst_pin) { this->rst_pin_ = rst_pin; } void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; } void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; } void set_shaping(uint8_t shaping) { this->shaping_ = shaping; } @@ -115,9 +115,9 @@ class SX126x : public Component, std::vector listeners_; std::vector packet_; std::vector sync_value_; - InternalGPIOPin *busy_pin_{nullptr}; - InternalGPIOPin *dio1_pin_{nullptr}; - InternalGPIOPin *rst_pin_{nullptr}; + GPIOPin *busy_pin_{nullptr}; + GPIOPin *dio1_pin_{nullptr}; + GPIOPin *rst_pin_{nullptr}; std::string hw_version_; char version_[16]; SX126xBw bandwidth_{SX126X_BW_125000}; diff --git a/esphome/components/sx127x/__init__.py b/esphome/components/sx127x/__init__.py index 33b556db078..77cb61f7f81 100644 --- a/esphome/components/sx127x/__init__.py +++ b/esphome/components/sx127x/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import spi import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_FREQUENCY, CONF_ID +from esphome.core import ID MULTI_CONF = True CODEOWNERS = ["@swoboda1337"] @@ -321,5 +322,8 @@ async def send_packet_action_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/sx127x/automation.h b/esphome/components/sx127x/automation.h index 2b9c261de11..fb0367fcca0 100644 --- a/esphome/components/sx127x/automation.h +++ b/esphome/components/sx127x/automation.h @@ -9,53 +9,59 @@ namespace sx127x { template class RunImageCalAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->run_image_cal(); } + void play(const Ts &...x) override { this->parent_->run_image_cal(); } }; template class SendPacketAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->transmit_packet(this->data_static_); + void play(const Ts &...x) override { + std::vector data; + if (this->len_ >= 0) { + // Static mode: copy from flash to vector + data.assign(this->data_.data, this->data_.data + this->len_); } else { - this->parent_->transmit_packet(this->data_func_(x...)); + // Template mode: call function + data = this->data_.func(x...); } + this->parent_->transmit_packet(data); } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; template class SetModeTxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_tx(); } + void play(const Ts &...x) override { this->parent_->set_mode_tx(); } }; template class SetModeRxAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_rx(); } + void play(const Ts &...x) override { this->parent_->set_mode_rx(); } }; template class SetModeSleepAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_sleep(); } + void play(const Ts &...x) override { this->parent_->set_mode_sleep(); } }; template class SetModeStandbyAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->set_mode_standby(); } + void play(const Ts &...x) override { this->parent_->set_mode_standby(); } }; } // namespace sx127x diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py index 5d2421fcbc9..256c7f276a2 100644 --- a/esphome/components/template/alarm_control_panel/__init__.py +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -137,7 +137,11 @@ async def to_code(config): cg.add(var.set_arming_night_time(config[CONF_ARMING_NIGHT_TIME])) supports_arm_night = True - for sensor in config.get(CONF_BINARY_SENSORS, []): + if sensors := config.get(CONF_BINARY_SENSORS, []): + # Initialize FixedVector with the exact number of sensors + cg.add(var.init_sensors(len(sensors))) + + for sensor in sensors: bs = await cg.get_variable(sensor[CONF_INPUT]) flags = BinarySensorFlags[FLAG_NORMAL] diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index f7e9872ce10..f025435261e 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -20,10 +20,13 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor, // Save the flags and type. Assign a store index for the per sensor data type. SensorDataStore sd; sd.last_chime_state = false; - this->sensor_map_[sensor].flags = flags; - this->sensor_map_[sensor].type = type; + AlarmSensor alarm_sensor; + alarm_sensor.sensor = sensor; + alarm_sensor.info.flags = flags; + alarm_sensor.info.type = type; + alarm_sensor.info.store_index = this->next_store_index_++; + this->sensors_.push_back(alarm_sensor); this->sensor_data_.push_back(sd); - this->sensor_map_[sensor].store_index = this->next_store_index_++; }; static const LogString *sensor_type_to_string(AlarmSensorType type) { @@ -42,25 +45,24 @@ static const LogString *sensor_type_to_string(AlarmSensorType type) { #endif void TemplateAlarmControlPanel::dump_config() { - ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:"); ESP_LOGCONFIG(TAG, + "TemplateAlarmControlPanel:\n" " Current State: %s\n" - " Number of Codes: %u", - LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size()); - if (!this->codes_.empty()) - ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->requires_code_to_arm_)); - ESP_LOGCONFIG(TAG, " Arming Away Time: %" PRIu32 "s", (this->arming_away_time_ / 1000)); - if (this->arming_home_time_ != 0) - ESP_LOGCONFIG(TAG, " Arming Home Time: %" PRIu32 "s", (this->arming_home_time_ / 1000)); - if (this->arming_night_time_ != 0) - ESP_LOGCONFIG(TAG, " Arming Night Time: %" PRIu32 "s", (this->arming_night_time_ / 1000)); - ESP_LOGCONFIG(TAG, + " Number of Codes: %zu\n" + " Requires Code To Arm: %s\n" + " Arming Away Time: %" PRIu32 "s\n" + " Arming Home Time: %" PRIu32 "s\n" + " Arming Night Time: %" PRIu32 "s\n" " Pending Time: %" PRIu32 "s\n" " Trigger Time: %" PRIu32 "s\n" " Supported Features: %" PRIu32, - (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); + LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size(), + YESNO(!this->codes_.empty() && this->requires_code_to_arm_), (this->arming_away_time_ / 1000), + (this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000), + (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { + const uint16_t flags = alarm_sensor.info.flags; ESP_LOGCONFIG(TAG, " Binary Sensor:\n" " Name: %s\n" @@ -69,11 +71,10 @@ void TemplateAlarmControlPanel::dump_config() { " Armed night bypass: %s\n" " Auto bypass: %s\n" " Chime mode: %s", - sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO), - TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME)); + alarm_sensor.sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(alarm_sensor.info.type)), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT), + TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_AUTO), TRUEFALSE(flags & BINARY_SENSOR_MODE_CHIME)); } #endif } @@ -123,7 +124,9 @@ void TemplateAlarmControlPanel::loop() { #ifdef USE_BINARY_SENSOR // Test all of the sensors regardless of the alarm panel state - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { + const auto &info = alarm_sensor.info; + auto *sensor = alarm_sensor.sensor; // Check for chime zones if (info.flags & BINARY_SENSOR_MODE_CHIME) { // Look for the transition from closed to open @@ -244,11 +247,11 @@ void TemplateAlarmControlPanel::arm_(optional code, alarm_control_p void TemplateAlarmControlPanel::bypass_before_arming() { #ifdef USE_BINARY_SENSOR - for (auto const &[sensor, info] : this->sensor_map_) { + for (const auto &alarm_sensor : this->sensors_) { // Check for faulted bypass_auto sensors and remove them from monitoring - if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) { - ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str()); - this->bypassed_sensor_indicies_.push_back(info.store_index); + if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) { + ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str()); + this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index); } } #endif diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 40a79004da0..80ce34b8ae7 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -1,11 +1,12 @@ #pragma once #include -#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" #include "esphome/components/alarm_control_panel/alarm_control_panel.h" @@ -49,7 +50,14 @@ struct SensorInfo { uint8_t store_index; }; -class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component { +#ifdef USE_BINARY_SENSOR +struct AlarmSensor { + binary_sensor::BinarySensor *sensor; + SensorInfo info; +}; +#endif + +class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component { public: TemplateAlarmControlPanel(); void dump_config() override; @@ -63,6 +71,12 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, void bypass_before_arming(); #ifdef USE_BINARY_SENSOR + /** Initialize the sensors vector with the specified capacity. + * + * @param capacity The number of sensors to allocate space for. + */ + void init_sensors(size_t capacity) { this->sensors_.init(capacity); } + /** Add a binary_sensor to the alarm_panel. * * @param sensor The BinarySensor instance. @@ -122,8 +136,8 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, protected: void control(const alarm_control_panel::AlarmControlPanelCall &call) override; #ifdef USE_BINARY_SENSOR - // This maps a binary sensor to its alarm specific info - std::map sensor_map_; + // List of binary sensors with their alarm-specific info + FixedVector sensors_; // a list of automatically bypassed sensors std::vector bypassed_sensor_indicies_; #endif diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index bc591391b98..0af709b097f 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { +class TemplateBinarySensor final : public Component, public binary_sensor::BinarySensor { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/button/template_button.h b/esphome/components/template/button/template_button.h index 68e976f64b3..5bda82c58f9 100644 --- a/esphome/components/template/button/template_button.h +++ b/esphome/components/template/button/template_button.h @@ -5,7 +5,7 @@ namespace esphome { namespace template_ { -class TemplateButton : public button::Button { +class TemplateButton final : public button::Button { public: // Implements the abstract `press_action` but the `on_press` trigger already handles the press. void press_action() override{}; diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index faff69f867b..125c67bb861 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -14,7 +14,7 @@ enum TemplateCoverRestoreMode { COVER_RESTORE_AND_CALL, }; -class TemplateCover : public cover::Cover, public Component { +class TemplateCover final : public cover::Cover, public Component { public: TemplateCover(); diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 7fed704d0ee..fe64b0ba14e 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateDate : public datetime::DateEntity, public PollingComponent { +class TemplateDate final : public datetime::DateEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ec45bf0326e..c44bd852652 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { +class TemplateDateTime final : public datetime::DateTimeEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index ea7474c0bad..0c95330d273 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateTime : public datetime::TimeEntity, public PollingComponent { +class TemplateTime final : public datetime::TimeEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/event/template_event.h b/esphome/components/template/event/template_event.h index 251ae9299b6..5467a641418 100644 --- a/esphome/components/template/event/template_event.h +++ b/esphome/components/template/event/template_event.h @@ -6,7 +6,7 @@ namespace esphome { namespace template_ { -class TemplateEvent : public Component, public event::Event {}; +class TemplateEvent final : public Component, public event::Event {}; } // namespace template_ } // namespace esphome diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index b09352f4d47..052b385b93a 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -6,7 +6,7 @@ namespace esphome { namespace template_ { -class TemplateFan : public Component, public fan::Fan { +class TemplateFan final : public Component, public fan::Fan { public: TemplateFan() {} void setup() override; diff --git a/esphome/components/template/lock/automation.h b/esphome/components/template/lock/automation.h index 61245465929..bd110b7b0c2 100644 --- a/esphome/components/template/lock/automation.h +++ b/esphome/components/template/lock/automation.h @@ -11,7 +11,7 @@ template class TemplateLockPublishAction : public Action, public: TEMPLATABLE_VALUE(lock::LockState, state) - void play(Ts... x) override { this->parent_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->parent_->publish_state(this->state_.value(x...)); } }; } // namespace template_ diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 14fca4635ec..ac10794e4d1 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateLock : public lock::Lock, public Component { +class TemplateLock final : public lock::Lock, public Component { public: TemplateLock(); diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index a9307e9246e..876ec96b3b8 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -9,7 +9,7 @@ namespace esphome { namespace template_ { -class TemplateNumber : public number::Number, public PollingComponent { +class TemplateNumber final : public number::Number, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/output/template_output.h b/esphome/components/template/output/template_output.h index 90de801a5c9..9ecfc446b97 100644 --- a/esphome/components/template/output/template_output.h +++ b/esphome/components/template/output/template_output.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateBinaryOutput : public output::BinaryOutput { +class TemplateBinaryOutput final : public output::BinaryOutput { public: Trigger *get_trigger() const { return trigger_; } @@ -17,7 +17,7 @@ class TemplateBinaryOutput : public output::BinaryOutput { Trigger *trigger_ = new Trigger(); }; -class TemplateFloatOutput : public output::FloatOutput { +class TemplateFloatOutput final : public output::FloatOutput { public: Trigger *get_trigger() const { return trigger_; } diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2dad059ade1..cb5b5469767 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -9,7 +9,7 @@ namespace esphome { namespace template_ { -class TemplateSelect : public select::Select, public PollingComponent { +class TemplateSelect final : public select::Select, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 793d754a0f8..3ca965dde37 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateSensor : public sensor::Sensor, public PollingComponent { +class TemplateSensor final : public sensor::Sensor, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 18a374df359..35c18af448d 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateSwitch : public switch_::Switch, public Component { +class TemplateSwitch final : public switch_::Switch, public Component { public: TemplateSwitch(); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index c12021f80eb..1a0a66ed5b5 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -60,7 +60,7 @@ template class TextSaver : public TemplateTextSaverBase { } }; -class TemplateText : public text::Text, public PollingComponent { +class TemplateText final : public text::Text, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 0d01c72023f..da5c518c7f2 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { +class TemplateTextSensor final : public text_sensor::TextSensor, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/valve/automation.h b/esphome/components/template/valve/automation.h index af9b070c607..e3f394ac7c8 100644 --- a/esphome/components/template/valve/automation.h +++ b/esphome/components/template/valve/automation.h @@ -11,7 +11,7 @@ template class TemplateValvePublishAction : public Action TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(valve::ValveOperation, current_operation) - void play(Ts... x) override { + void play(const Ts &...x) override { if (this->position_.has_value()) this->parent_->position = this->position_.value(x...); if (this->current_operation_.has_value()) diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index d6235f8e5cc..c452648193e 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -14,7 +14,7 @@ enum TemplateValveRestoreMode { VALVE_RESTORE_AND_CALL, }; -class TemplateValve : public valve::Valve, public Component { +class TemplateValve final : public valve::Valve, public Component { public: TemplateValve(); diff --git a/esphome/components/text/automation.h b/esphome/components/text/automation.h index f20a4f433b6..e7667fe491f 100644 --- a/esphome/components/text/automation.h +++ b/esphome/components/text/automation.h @@ -19,7 +19,7 @@ template class TextSetAction : public Action { explicit TextSetAction(Text *text) : text_(text) {} TEMPLATABLE_VALUE(std::string, value) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->text_->make_call(); call.set_value(this->value_.value(x...)); call.perform(); diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index 654893d4e49..933d82c85c1 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -1,4 +1,6 @@ #include "text.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -16,6 +18,9 @@ void Text::publish_state(const std::string &state) { ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); } this->state_callback_.call(state); +#if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_text_update(this); +#endif } void Text::add_on_state_callback(std::function &&callback) { diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index d7286845e09..709c54c1409 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -29,7 +29,7 @@ template class TextSensorStateCondition : public Conditionparent_->state == this->state_.value(x...); } + bool check(const Ts &...x) override { return this->parent_->state == this->state_.value(x...); } protected: TextSensor *parent_; @@ -40,7 +40,7 @@ template class TextSensorPublishAction : public Action { TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(std::string, state) - void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } + void play(const Ts &...x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: TextSensor *sensor_; diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 0294d65861c..65d7b1f0be9 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -1,4 +1,6 @@ #include "text_sensor.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -24,9 +26,9 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text void TextSensor::publish_state(const std::string &state) { this->raw_state = state; - if (this->raw_callback_) { - this->raw_callback_->call(state); - } + + // Call raw callbacks (before filters) + this->callbacks_.call_first(this->raw_count_, state); ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str()); @@ -68,13 +70,11 @@ void TextSensor::clear_filters() { } void TextSensor::add_on_state_callback(std::function callback) { - this->callback_.add(std::move(callback)); + this->callbacks_.add_second(std::move(callback)); } + void TextSensor::add_on_raw_state_callback(std::function callback) { - if (!this->raw_callback_) { - this->raw_callback_ = make_unique>(); - } - this->raw_callback_->add(std::move(callback)); + this->callbacks_.add_first(std::move(callback), &this->raw_count_); } std::string TextSensor::get_state() const { return this->state; } @@ -83,7 +83,13 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) { this->state = state; this->set_has_state(true); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); - this->callback_.call(state); + + // Call filtered callbacks (after filters) + this->callbacks_.call_second(this->raw_count_, state); + +#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_text_sensor_update(this); +#endif } } // namespace text_sensor diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index db2e857ae37..1f4f3170e04 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -58,11 +58,11 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass { void internal_send_state_to_frontend(const std::string &state); protected: - std::unique_ptr> - raw_callback_; ///< Storage for raw state callbacks (lazy allocated). - CallbackManager callback_; ///< Storage for filtered state callbacks. + PartitionedCallbackManager callbacks_; Filter *filter_list_{nullptr}; ///< Store all active filters. + + uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector) }; } // namespace text_sensor diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index a928d208f3a..a3c155aac06 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -945,6 +945,10 @@ async def to_code(config): cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS])) if CONF_PRESET in config: + # Separate standard and custom presets, and build preset config variables + standard_presets: list[tuple[cg.MockObj, cg.MockObj]] = [] + custom_presets: list[tuple[str, cg.MockObj]] = [] + for preset_config in config[CONF_PRESET]: name = preset_config[CONF_NAME] standard_preset = None @@ -987,9 +991,39 @@ async def to_code(config): ) if standard_preset is not None: - cg.add(var.set_preset_config(standard_preset, preset_target_variable)) + standard_presets.append((standard_preset, preset_target_variable)) else: - cg.add(var.set_custom_preset_config(name, preset_target_variable)) + custom_presets.append((name, preset_target_variable)) + + # Build initializer list for standard presets + if standard_presets: + cg.add( + var.set_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatPresetEntry"), + ("preset", preset), + ("config", preset_var), + ) + for preset, preset_var in standard_presets + ] + ) + ) + + # Build initializer list for custom presets + if custom_presets: + cg.add( + var.set_custom_preset_config( + [ + cg.StructInitializer( + thermostat_ns.struct("ThermostatCustomPresetEntry"), + ("name", cg.RawExpression(f'"{name}"')), + ("config", preset_var), + ) + for name, preset_var in custom_presets + ] + ) + ) if CONF_DEFAULT_PRESET in config: default_preset_name = config[CONF_DEFAULT_PRESET] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 8258fa9d653..2b51f58f4ff 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -53,7 +53,7 @@ void ThermostatClimate::setup() { if (use_default_preset) { if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) { this->change_preset_(this->default_preset_); - } else if (!this->default_custom_preset_.empty()) { + } else if (this->default_custom_preset_ != nullptr) { this->change_custom_preset_(this->default_custom_preset_); } } @@ -218,13 +218,13 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { this->preset = call.get_preset().value(); } } - if (call.get_custom_preset().has_value()) { + if (call.has_custom_preset()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_custom_preset_(call.get_custom_preset().value()); + this->change_custom_preset_(call.get_custom_preset()); } else { // Use the base class method which handles pointer lookup internally - this->set_custom_preset_(call.get_custom_preset().value().c_str()); + this->set_custom_preset_(call.get_custom_preset()); } } @@ -319,16 +319,16 @@ climate::ClimateTraits ThermostatClimate::traits() { if (this->supports_swing_mode_vertical_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); - for (auto &it : this->preset_config_) { - traits.add_supported_preset(it.first); + for (const auto &entry : this->preset_config_) { + traits.add_supported_preset(entry.preset); } - // Extract custom preset names from the custom_preset_config_ map + // Extract custom preset names from the custom_preset_config_ vector if (!this->custom_preset_config_.empty()) { std::vector custom_preset_names; custom_preset_names.reserve(this->custom_preset_config_.size()); - for (const auto &it : this->custom_preset_config_) { - custom_preset_names.push_back(it.first.c_str()); + for (const auto &entry : this->custom_preset_config_) { + custom_preset_names.push_back(entry.name); } traits.set_supported_custom_presets(custom_preset_names); } @@ -1154,12 +1154,18 @@ void ThermostatClimate::dump_preset_config_(const char *preset_name, const Therm } void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { - auto config = this->preset_config_.find(preset); + // Linear search through preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->preset_config_) { + if (entry.preset == preset) { + config = &entry.config; + break; + } + } - if (config != this->preset_config_.end()) { + if (config != nullptr) { ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); - if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) || - this->preset.value() != preset) { + if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; this->set_preset_(preset); @@ -1177,31 +1183,38 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { } } -void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) { - auto config = this->custom_preset_config_.find(custom_preset); +void ThermostatClimate::change_custom_preset_(const char *custom_preset) { + // Linear search through custom preset configurations + const ThermostatClimateTargetTempConfig *config = nullptr; + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + config = &entry.config; + break; + } + } - if (config != this->custom_preset_config_.end()) { - ESP_LOGV(TAG, "Custom preset %s requested", custom_preset.c_str()); - if (this->change_preset_internal_(config->second) || !this->has_custom_preset() || - strcmp(this->get_custom_preset(), custom_preset.c_str()) != 0) { + if (config != nullptr) { + ESP_LOGV(TAG, "Custom preset %s requested", custom_preset); + if (this->change_preset_internal_(*config) || !this->has_custom_preset() || + strcmp(this->get_custom_preset(), custom_preset) != 0) { // Fire any preset changed trigger if defined Trigger<> *trig = this->preset_change_trigger_; // Use the base class method which handles pointer lookup and preset reset internally - this->set_custom_preset_(custom_preset.c_str()); + this->set_custom_preset_(custom_preset); if (trig != nullptr) { trig->trigger(); } this->refresh(); - ESP_LOGI(TAG, "Custom preset %s applied", custom_preset.c_str()); + ESP_LOGI(TAG, "Custom preset %s applied", custom_preset); } else { - ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset.c_str()); + ESP_LOGI(TAG, "No changes required to apply custom preset %s", custom_preset); + // Note: set_custom_preset_() above handles preset.reset() and custom_preset_ assignment internally. + // The old code had these lines here unconditionally, which was a bug (double assignment, state modification + // even when no changes were needed). Now properly handled by the protected setter with mutual exclusion. } - // Note: set_custom_preset_() above handles preset.reset() and custom_preset_ assignment internally. - // The old code had these lines here unconditionally, which was a bug (double assignment, state modification - // even when no changes were needed). Now properly handled by the protected setter with mutual exclusion. } else { - ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset.c_str()); + ESP_LOGW(TAG, "Custom preset %s not configured; ignoring", custom_preset); } } @@ -1247,14 +1260,12 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem return something_changed; } -void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, - const ThermostatClimateTargetTempConfig &config) { - this->preset_config_[preset] = config; +void ThermostatClimate::set_preset_config(std::initializer_list presets) { + this->preset_config_ = presets; } -void ThermostatClimate::set_custom_preset_config(const std::string &name, - const ThermostatClimateTargetTempConfig &config) { - this->custom_preset_config_[name] = config; +void ThermostatClimate::set_custom_preset_config(std::initializer_list presets) { + this->custom_preset_config_ = presets; } ThermostatClimate::ThermostatClimate() @@ -1293,8 +1304,16 @@ ThermostatClimate::ThermostatClimate() humidity_control_humidify_action_trigger_(new Trigger<>()), humidity_control_off_action_trigger_(new Trigger<>()) {} -void ThermostatClimate::set_default_preset(const std::string &custom_preset) { - this->default_custom_preset_ = custom_preset; +void ThermostatClimate::set_default_preset(const char *custom_preset) { + // Find the preset in custom_preset_config_ and store pointer from there + for (const auto &entry : this->custom_preset_config_) { + if (strcmp(entry.name, custom_preset) == 0) { + this->default_custom_preset_ = entry.name; + return; + } + } + // If not found, it will be caught during validation + this->default_custom_preset_ = nullptr; } void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } @@ -1605,19 +1624,22 @@ void ThermostatClimate::dump_config() { if (!this->preset_config_.empty()) { ESP_LOGCONFIG(TAG, " Supported PRESETS:"); - for (auto &it : this->preset_config_) { - const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); - ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_preset_ ? " (default)" : ""); - this->dump_preset_config_(preset_name, it.second); + for (const auto &entry : this->preset_config_) { + const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset)); + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : ""); + this->dump_preset_config_(preset_name, entry.config); } } if (!this->custom_preset_config_.empty()) { ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:"); - for (auto &it : this->custom_preset_config_) { - const auto *preset_name = it.first.c_str(); - ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_custom_preset_ ? " (default)" : ""); - this->dump_preset_config_(preset_name, it.second); + for (const auto &entry : this->custom_preset_config_) { + const auto *preset_name = entry.name; + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, + (this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0) + ? " (default)" + : ""); + this->dump_preset_config_(preset_name, entry.config); } } } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 42adab7751e..76391f800cb 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -3,12 +3,12 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" #include #include -#include namespace esphome { namespace thermostat { @@ -72,14 +72,29 @@ struct ThermostatClimateTargetTempConfig { optional mode_{}; }; +/// Entry for standard preset lookup +struct ThermostatPresetEntry { + climate::ClimatePreset preset; + ThermostatClimateTargetTempConfig config; +}; + +/// Entry for custom preset lookup +struct ThermostatCustomPresetEntry { + const char *name; + ThermostatClimateTargetTempConfig config; +}; + class ThermostatClimate : public climate::Climate, public Component { public: + using PresetEntry = ThermostatPresetEntry; + using CustomPresetEntry = ThermostatCustomPresetEntry; + ThermostatClimate(); void setup() override; void dump_config() override; void loop() override; - void set_default_preset(const std::string &custom_preset); + void set_default_preset(const char *custom_preset); void set_default_preset(climate::ClimatePreset preset); void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from); void set_set_point_minimum_differential(float differential); @@ -131,8 +146,8 @@ class ThermostatClimate : public climate::Climate, public Component { void set_supports_humidification(bool supports_humidification); void set_supports_two_points(bool supports_two_points); - void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); - void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); + void set_preset_config(std::initializer_list presets); + void set_custom_preset_config(std::initializer_list presets); Trigger<> *get_cool_action_trigger() const; Trigger<> *get_supplemental_cool_action_trigger() const; @@ -199,7 +214,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly void change_preset_(climate::ClimatePreset preset); /// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly - void change_custom_preset_(const std::string &custom_preset); + void change_custom_preset_(const char *custom_preset); /// Applies the temperature, mode, fan, and swing modes of the provided config. /// This is agnostic of custom vs built in preset @@ -516,9 +531,6 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_swing_mode_trigger_{nullptr}; Trigger<> *prev_humidity_control_trigger_{nullptr}; - /// Default custom preset to use on start up - std::string default_custom_preset_{}; - /// Climate action timers std::array timer_{ ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), @@ -534,9 +546,12 @@ class ThermostatClimate : public climate::Climate, public Component { }; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) - std::map preset_config_{}; + FixedVector preset_config_{}; /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") - std::map custom_preset_config_{}; + FixedVector custom_preset_config_{}; + /// Default custom preset to use on start up (pointer to entry in custom_preset_config_) + private: + const char *default_custom_preset_{nullptr}; }; } // namespace thermostat diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 42c564659f7..175cee0c1ff 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -23,6 +23,13 @@ namespace time { static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; + +void RealTimeClock::dump_config() { +#ifdef USE_TIME_TIMEZONE + ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str()); +#endif +} + void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); // Update UTC epoch time. diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 7e60bbd234a..2f17bd86d6f 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -52,6 +52,8 @@ class RealTimeClock : public PollingComponent { this->time_sync_callback_.add(std::move(callback)); }; + void dump_config() override; + protected: /// Report a unix epoch as current time. void synchronize_epoch_(uint32_t epoch); @@ -67,7 +69,7 @@ class RealTimeClock : public PollingComponent { template class TimeHasTimeCondition : public Condition { public: TimeHasTimeCondition(RealTimeClock *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->now().is_valid(); } + bool check(const Ts &...x) override { return this->parent_->now().is_valid(); } protected: RealTimeClock *parent_; diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py new file mode 100644 index 00000000000..72afc18387b --- /dev/null +++ b/esphome/components/tinyusb/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +from esphome.components import esp32 +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option +from esphome.components.esp32.const import ( + VARIANT_ESP32P4, + VARIANT_ESP32S2, + VARIANT_ESP32S3, +) +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@kbx81"] +CONFLICTS_WITH = ["usb_host"] + +CONF_USB_LANG_ID = "usb_lang_id" +CONF_USB_MANUFACTURER_STR = "usb_manufacturer_str" +CONF_USB_PRODUCT_ID = "usb_product_id" +CONF_USB_PRODUCT_STR = "usb_product_str" +CONF_USB_SERIAL_STR = "usb_serial_str" +CONF_USB_VENDOR_ID = "usb_vendor_id" + +tinyusb_ns = cg.esphome_ns.namespace("tinyusb") +TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(TinyUSB), + cv.Optional(CONF_USB_PRODUCT_ID, default=0x4001): cv.uint16_t, + cv.Optional(CONF_USB_VENDOR_ID, default=0x303A): cv.uint16_t, + cv.Optional(CONF_USB_LANG_ID, default=0x0409): cv.uint16_t, + cv.Optional(CONF_USB_MANUFACTURER_STR, default="ESPHome"): cv.string, + cv.Optional(CONF_USB_PRODUCT_STR, default="ESPHome"): cv.string, + cv.Optional(CONF_USB_SERIAL_STR, default=""): cv.string, + } + ).extend(cv.COMPONENT_SCHEMA), + esp32.only_on_variant( + supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3], + ), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + # Set USB device descriptor properties + cg.add(var.set_usb_desc_product_id(config[CONF_USB_PRODUCT_ID])) + cg.add(var.set_usb_desc_vendor_id(config[CONF_USB_VENDOR_ID])) + cg.add(var.set_usb_desc_lang_id(config[CONF_USB_LANG_ID])) + cg.add(var.set_usb_desc_manufacturer(config[CONF_USB_MANUFACTURER_STR])) + cg.add(var.set_usb_desc_product(config[CONF_USB_PRODUCT_STR])) + if config[CONF_USB_SERIAL_STR]: + cg.add(var.set_usb_desc_serial(config[CONF_USB_SERIAL_STR])) + + add_idf_component(name="espressif/esp_tinyusb", ref="1.7.6~1") + + add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID", False) + add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_DEFAULT_PID", False) + add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_BCD_DEVICE", 0x0100) diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp new file mode 100644 index 00000000000..a2057c90cea --- /dev/null +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -0,0 +1,44 @@ +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "tinyusb_component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::tinyusb { + +static const char *TAG = "tinyusb"; + +void TinyUSB::setup() { + // Use the device's MAC address as its serial number if no serial number is defined + if (this->string_descriptor_[SERIAL_NUMBER] == nullptr) { + static char mac_addr_buf[13]; + get_mac_address_into_buffer(mac_addr_buf); + this->string_descriptor_[SERIAL_NUMBER] = mac_addr_buf; + } + + this->tusb_cfg_ = { + .descriptor = &this->usb_descriptor_, + .string_descriptor = this->string_descriptor_, + .string_descriptor_count = SIZE, + .external_phy = false, + }; + + esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_); + if (result != ESP_OK) { + this->mark_failed(); + } +} + +void TinyUSB::dump_config() { + ESP_LOGCONFIG(TAG, + "TinyUSB:\n" + " Product ID: 0x%04X\n" + " Vendor ID: 0x%04X\n" + " Manufacturer: '%s'\n" + " Product: '%s'\n" + " Serial: '%s'\n", + this->usb_descriptor_.idProduct, this->usb_descriptor_.idVendor, this->string_descriptor_[MANUFACTURER], + this->string_descriptor_[PRODUCT], this->string_descriptor_[SERIAL_NUMBER]); +} + +} // namespace esphome::tinyusb +#endif diff --git a/esphome/components/tinyusb/tinyusb_component.h b/esphome/components/tinyusb/tinyusb_component.h new file mode 100644 index 00000000000..56c286f4559 --- /dev/null +++ b/esphome/components/tinyusb/tinyusb_component.h @@ -0,0 +1,72 @@ +#pragma once +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "esphome/core/component.h" + +#include "tinyusb.h" +#include "tusb.h" + +namespace esphome::tinyusb { + +enum USBDStringDescriptor : uint8_t { + LANGUAGE_ID = 0, + MANUFACTURER = 1, + PRODUCT = 2, + SERIAL_NUMBER = 3, + INTERFACE = 4, + TERMINATOR = 5, + SIZE = 6, +}; + +static const char *DEFAULT_USB_STR = "ESPHome"; + +class TinyUSB : public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::BUS; } + + void set_usb_desc_product_id(uint16_t product_id) { this->usb_descriptor_.idProduct = product_id; } + void set_usb_desc_vendor_id(uint16_t vendor_id) { this->usb_descriptor_.idVendor = vendor_id; } + void set_usb_desc_lang_id(uint16_t lang_id) { + this->usb_desc_lang_id_[0] = lang_id & 0xFF; + this->usb_desc_lang_id_[1] = lang_id >> 8; + } + void set_usb_desc_manufacturer(const char *usb_desc_manufacturer) { + this->string_descriptor_[MANUFACTURER] = usb_desc_manufacturer; + } + void set_usb_desc_product(const char *usb_desc_product) { this->string_descriptor_[PRODUCT] = usb_desc_product; } + void set_usb_desc_serial(const char *usb_desc_serial) { this->string_descriptor_[SERIAL_NUMBER] = usb_desc_serial; } + + protected: + char usb_desc_lang_id_[2] = {0x09, 0x04}; // defaults to english + + const char *string_descriptor_[SIZE] = { + this->usb_desc_lang_id_, // 0: supported language is English (0x0409) + DEFAULT_USB_STR, // 1: Manufacturer + DEFAULT_USB_STR, // 2: Product + nullptr, // 3: Serial Number + nullptr, // 4: Interface + nullptr, // 5: Terminator + }; + + tinyusb_config_t tusb_cfg_{}; + tusb_desc_device_t usb_descriptor_{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = 0x303A, + .idProduct = 0x4001, + .bcdDevice = CONFIG_TINYUSB_DESC_BCD_DEVICE, + .iManufacturer = 1, + .iProduct = 2, + .iSerialNumber = 3, + .bNumConfigurations = 1, + }; +}; + +} // namespace esphome::tinyusb +#endif diff --git a/esphome/components/tm1621/tm1621.h b/esphome/components/tm1621/tm1621.h index b9f330e96e3..fe923417a60 100644 --- a/esphome/components/tm1621/tm1621.h +++ b/esphome/components/tm1621/tm1621.h @@ -3,13 +3,14 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/components/display/display.h" namespace esphome { namespace tm1621 { class TM1621Display; -using tm1621_writer_t = std::function; +using tm1621_writer_t = display::DisplayWriter; class TM1621Display : public PollingComponent { public: @@ -59,7 +60,7 @@ class TM1621Display : public PollingComponent { GPIOPin *cs_pin_; GPIOPin *read_pin_; GPIOPin *write_pin_; - optional writer_{}; + tm1621_writer_t writer_{}; char row_[2][12]; uint8_t state_; uint8_t device_; diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index d44680c6231..b9e96119e9b 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -4,6 +4,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/time.h" +#include "esphome/components/display/display.h" #include @@ -19,7 +20,7 @@ class TM1637Display; class TM1637Key; #endif -using tm1637_writer_t = std::function; +using tm1637_writer_t = display::DisplayWriter; class TM1637Display : public PollingComponent { public: @@ -78,7 +79,7 @@ class TM1637Display : public PollingComponent { uint8_t length_; bool inverted_; bool on_{true}; - optional writer_{}; + tm1637_writer_t writer_{}; uint8_t buffer_[6] = {0}; #ifdef USE_BINARY_SENSOR std::vector tm1637_keys_{}; diff --git a/esphome/components/tm1638/tm1638.h b/esphome/components/tm1638/tm1638.h index add72cfdf38..f6b2922ecfb 100644 --- a/esphome/components/tm1638/tm1638.h +++ b/esphome/components/tm1638/tm1638.h @@ -5,6 +5,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/time.h" +#include "esphome/components/display/display.h" #include @@ -18,7 +19,7 @@ class KeyListener { class TM1638Component; -using tm1638_writer_t = std::function; +using tm1638_writer_t = display::DisplayWriter; class TM1638Component : public PollingComponent { public: @@ -70,7 +71,7 @@ class TM1638Component : public PollingComponent { GPIOPin *stb_pin_; GPIOPin *dio_pin_; uint8_t *buffer_ = new uint8_t[8]; - optional writer_{}; + tm1638_writer_t writer_{}; std::vector listeners_{}; }; diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index 7079910adf5..83e74c5f330 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -61,7 +61,7 @@ template class SetBrightnessAction : public Action, publi public: TEMPLATABLE_VALUE(uint8_t, brightness) - void play(Ts... x) override { + void play(const Ts &...x) override { auto brightness = this->brightness_.value(x...); this->parent_->set_brightness(brightness); } @@ -71,7 +71,7 @@ template class SetLevelAction : public Action, public Par public: TEMPLATABLE_VALUE(uint8_t, level) - void play(Ts... x) override { + void play(const Ts &...x) override { auto level = this->level_.value(x...); this->parent_->set_level(level); } @@ -81,7 +81,7 @@ template class SetLevelPercentAction : public Action, pub public: TEMPLATABLE_VALUE(uint8_t, level_percent) - void play(Ts... x) override { + void play(const Ts &...x) override { auto level_percent = this->level_percent_.value(x...); this->parent_->set_level_percent(level_percent); } @@ -89,12 +89,12 @@ template class SetLevelPercentAction : public Action, pub template class TurnOnAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->turn_on(); } + void play(const Ts &...x) override { this->parent_->turn_on(); } }; template class TurnOffAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->turn_off(); } + void play(const Ts &...x) override { this->parent_->turn_off(); } }; } // namespace tm1651 diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index 07e3ce44eed..9d46e4c8ca0 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -21,23 +21,17 @@ void TuyaSelect::setup() { }); } -void TuyaSelect::control(const std::string &value) { +void TuyaSelect::control(size_t index) { if (this->optimistic_) - this->publish_state(value); + this->publish_state(index); - auto idx = this->index_of(value); - if (idx.has_value()) { - uint8_t mapping = this->mappings_.at(idx.value()); - ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, value.c_str()); - if (this->is_int_) { - this->parent_->set_integer_datapoint_value(this->select_id_, mapping); - } else { - this->parent_->set_enum_datapoint_value(this->select_id_, mapping); - } - return; + uint8_t mapping = this->mappings_.at(index); + ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, this->option_at(index)); + if (this->is_int_) { + this->parent_->set_integer_datapoint_value(this->select_id_, mapping); + } else { + this->parent_->set_enum_datapoint_value(this->select_id_, mapping); } - - ESP_LOGW(TAG, "Invalid value %s", value.c_str()); } void TuyaSelect::dump_config() { diff --git a/esphome/components/tuya/select/tuya_select.h b/esphome/components/tuya/select/tuya_select.h index 12d7b507d4b..24505c99104 100644 --- a/esphome/components/tuya/select/tuya_select.h +++ b/esphome/components/tuya/select/tuya_select.h @@ -23,7 +23,7 @@ class TuyaSelect : public select::Select, public Component { void set_select_mappings(std::vector mappings) { this->mappings_ = std::move(mappings); } protected: - void control(const std::string &value) override; + void control(size_t index) override; Tuya *parent_; bool optimistic_ = false; diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index eb911ed0073..7b0d9726b84 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,3 +1,4 @@ +from logging import getLogger import math import re @@ -31,10 +32,12 @@ from esphome.const import ( PLATFORM_HOST, PlatformFramework, ) -from esphome.core import CORE +from esphome.core import CORE, ID import esphome.final_validate as fv from esphome.yaml_util import make_data_base +_LOGGER = getLogger(__name__) + CODEOWNERS = ["@esphome/core"] uart_ns = cg.esphome_ns.namespace("uart") UARTComponent = uart_ns.class_("UARTComponent") @@ -130,6 +133,21 @@ def validate_host_config(config): return config +def validate_rx_buffer_size(config): + if CORE.is_esp32: + # ESP32 UART hardware FIFO is 128 bytes (LP UART is 16 bytes, but we use 128 as safe minimum) + # rx_buffer_size must be greater than the hardware FIFO length + min_buffer_size = 128 + if config[CONF_RX_BUFFER_SIZE] <= min_buffer_size: + _LOGGER.warning( + "UART rx_buffer_size (%d bytes) is too small and must be greater than the hardware " + "FIFO size (%d bytes). The buffer size will be automatically adjusted at runtime.", + config[CONF_RX_BUFFER_SIZE], + min_buffer_size, + ) + return config + + def _uart_declare_type(value): if CORE.is_esp8266: return cv.declare_id(ESP8266UartComponent)(value) @@ -247,6 +265,7 @@ CONFIG_SCHEMA = cv.All( ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT), validate_host_config, + validate_rx_buffer_size, ) @@ -446,7 +465,10 @@ async def uart_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(cg.ArrayInitializer(*data))) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index 9c599253de1..c2eb308eb8d 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -10,32 +10,35 @@ namespace uart { template class UARTWriteAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; - } - void set_data_static(std::vector &&data) { - this->data_static_ = std::move(data); - this->static_ = true; - } - void set_data_static(std::initializer_list data) { - this->data_static_ = std::vector(data); - this->static_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + // Stateless lambdas (generated by ESPHome) implicitly convert to function pointers + this->code_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->write_array(this->data_static_); + // Store pointer to static data in flash (no RAM copy) + void set_data_static(const uint8_t *data, size_t len) { + this->code_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void play(const Ts &...x) override { + if (this->len_ >= 0) { + // Static mode: use pointer and length + this->parent_->write_array(this->code_.data, static_cast(this->len_)); } else { - auto val = this->data_func_(x...); + // Template mode: call function + auto val = this->code_.func(x...); this->parent_->write_array(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Code { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } code_; }; } // namespace uart diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 73813d2d5b7..70a13c9e378 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -91,6 +91,16 @@ void IDFUARTComponent::setup() { this->uart_num_ = static_cast(next_uart_num++); this->lock_ = xSemaphoreCreateMutex(); +#if (SOC_UART_LP_NUM >= 1) + size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN); +#else + size_t fifo_len = SOC_UART_FIFO_LEN; +#endif + if (this->rx_buffer_size_ <= fifo_len) { + ESP_LOGW(TAG, "rx_buffer_size is too small, must be greater than %zu", fifo_len); + this->rx_buffer_size_ = fifo_len * 2; + } + xSemaphoreTake(this->lock_, portMAX_DELAY); this->load_settings(false); @@ -237,8 +247,12 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) { void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_write_bytes(this->uart_num_, data, len); + int32_t write_len = uart_write_bytes(this->uart_num_, data, len); xSemaphoreGive(this->lock_); + if (write_len != (int32_t) len) { + ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len); + this->mark_failed(); + } #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_TX, data[i]); @@ -267,6 +281,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { size_t length_to_read = len; + int32_t read_len = 0; if (!this->check_read_timeout_(len)) return false; xSemaphoreTake(this->lock_, portMAX_DELAY); @@ -277,25 +292,31 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { this->has_peek_ = false; } if (length_to_read > 0) - uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); + read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS); xSemaphoreGive(this->lock_); #ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } #endif - return true; + return read_len == (int32_t) length_to_read; } int IDFUARTComponent::available() { - size_t available; + size_t available = 0; + esp_err_t err; xSemaphoreTake(this->lock_, portMAX_DELAY); - uart_get_buffered_data_len(this->uart_num_, &available); - if (this->has_peek_) - available++; + err = uart_get_buffered_data_len(this->uart_num_, &available); xSemaphoreGive(this->lock_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err)); + this->mark_failed(); + } + if (this->has_peek_) { + available++; + } return available; } diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 6b1e4f8ed87..69abf4b989e 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -12,7 +12,7 @@ from esphome.components.packet_transport import ( ) import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID -from esphome.core import Lambda +from esphome.core import ID, Lambda from esphome.cpp_generator import ExpressionStatement, MockObj CODEOWNERS = ["@clydebarrow"] @@ -158,5 +158,8 @@ async def udp_write_to_code(config, action_id, template_arg, args): templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) cg.add(var.set_data_template(templ)) else: - cg.add(var.set_data_static(data)) + # Generate static array in flash to avoid RAM copy + arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8) + arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data)) + cg.add(var.set_data_static(arr, len(data))) return var diff --git a/esphome/components/udp/automation.h b/esphome/components/udp/automation.h index f75e6d35bf2..b66c2a9892a 100644 --- a/esphome/components/udp/automation.h +++ b/esphome/components/udp/automation.h @@ -11,28 +11,33 @@ namespace udp { template class UDPWriteAction : public Action, public Parented { public: - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; - } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; + void set_data_template(std::vector (*func)(Ts...)) { + this->data_.func = func; + this->len_ = -1; // Sentinel value indicates template mode } - void play(Ts... x) override { - if (this->static_) { - this->parent_->send_packet(this->data_static_); + void set_data_static(const uint8_t *data, size_t len) { + this->data_.data = data; + this->len_ = len; // Length >= 0 indicates static mode + } + + void play(const Ts &...x) override { + if (this->len_ >= 0) { + // Static mode: pass pointer directly to send_packet(const uint8_t *, size_t) + this->parent_->send_packet(this->data_.data, static_cast(this->len_)); } else { - auto val = this->data_func_(x...); + // Template mode: call function and pass vector to send_packet(const std::vector &) + auto val = this->data_.func(x...); this->parent_->send_packet(val); } } protected: - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; + ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length + union Data { + std::vector (*func)(Ts...); // Function pointer (stateless lambdas) + const uint8_t *data; // Pointer to static data in flash + } data_; }; } // namespace udp diff --git a/esphome/components/ufire_ec/ufire_ec.h b/esphome/components/ufire_ec/ufire_ec.h index 3d436555a2b..bfbed1b43e0 100644 --- a/esphome/components/ufire_ec/ufire_ec.h +++ b/esphome/components/ufire_ec/ufire_ec.h @@ -65,7 +65,7 @@ template class UFireECCalibrateProbeAction : public Actionparent_->calibrate_probe(this->solution_.value(x...), this->temperature_.value(x...)); } @@ -77,7 +77,7 @@ template class UFireECResetAction : public Action { public: UFireECResetAction(UFireECComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset_board(); } + void play(const Ts &...x) override { this->parent_->reset_board(); } protected: UFireECComponent *parent_; diff --git a/esphome/components/ufire_ise/ufire_ise.h b/esphome/components/ufire_ise/ufire_ise.h index 01efdcdb557..fe9a6dfb9c8 100644 --- a/esphome/components/ufire_ise/ufire_ise.h +++ b/esphome/components/ufire_ise/ufire_ise.h @@ -64,7 +64,7 @@ template class UFireISECalibrateProbeLowAction : public Actionparent_->calibrate_probe_low(this->solution_.value(x...)); } + void play(const Ts &...x) override { this->parent_->calibrate_probe_low(this->solution_.value(x...)); } protected: UFireISEComponent *parent_; @@ -75,7 +75,7 @@ template class UFireISECalibrateProbeHighAction : public Action< UFireISECalibrateProbeHighAction(UFireISEComponent *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, solution) - void play(Ts... x) override { this->parent_->calibrate_probe_high(this->solution_.value(x...)); } + void play(const Ts &...x) override { this->parent_->calibrate_probe_high(this->solution_.value(x...)); } protected: UFireISEComponent *parent_; @@ -85,7 +85,7 @@ template class UFireISEResetAction : public Action { public: UFireISEResetAction(UFireISEComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset_board(); } + void play(const Ts &...x) override { this->parent_->reset_board(); } protected: UFireISEComponent *parent_; diff --git a/esphome/components/update/automation.h b/esphome/components/update/automation.h index df50f86a0cd..8563b855fe4 100644 --- a/esphome/components/update/automation.h +++ b/esphome/components/update/automation.h @@ -11,12 +11,12 @@ template class PerformAction : public Action, public Pare TEMPLATABLE_VALUE(bool, force) public: - void play(Ts... x) override { this->parent_->perform(this->force_.value(x...)); } + void play(const Ts &...x) override { this->parent_->perform(this->force_.value(x...)); } }; template class IsAvailableCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } + bool check(const Ts &...x) override { return this->parent_->state == UPDATE_STATE_AVAILABLE; } }; } // namespace update diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index ce97fb1b77f..567fc9fc8e0 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -1,5 +1,6 @@ #include "update_entity.h" - +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" namespace esphome { @@ -32,6 +33,9 @@ void UpdateEntity::publish_state() { this->set_has_state(true); this->state_callback_.call(); +#if defined(USE_UPDATE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_update(this); +#endif } } // namespace update diff --git a/esphome/components/usb_host/__init__.py b/esphome/components/usb_host/__init__.py index d452e0e9fa8..cccabcf646c 100644 --- a/esphome/components/usb_host/__init__.py +++ b/esphome/components/usb_host/__init__.py @@ -1,4 +1,5 @@ import esphome.codegen as cg +from esphome.components import socket from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, @@ -11,7 +12,7 @@ from esphome.const import CONF_DEVICES, CONF_ID from esphome.cpp_types import Component from esphome.types import ConfigType -AUTO_LOAD = ["bytebuffer"] +AUTO_LOAD = ["bytebuffer", "socket"] CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["esp32"] usb_host_ns = cg.esphome_ns.namespace("usb_host") @@ -71,6 +72,11 @@ 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 dc216a209db..4c09cf8a498 100644 --- a/esphome/components/usb_host/usb_host_client.cpp +++ b/esphome/components/usb_host/usb_host_client.cpp @@ -3,6 +3,7 @@ #include "usb_host.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" +#include "esphome/core/application.h" #include "esphome/components/bytebuffer/bytebuffer.h" #include @@ -174,6 +175,11 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void * // Push to lock-free queue (always succeeds since pool size == queue size) client->event_queue.push(event); + + // 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) + App.wake_loop_threadsafe(); +#endif } void USBClient::setup() { usb_host_client_config_t config{.is_synchronous = false, diff --git a/esphome/components/valve/automation.h b/esphome/components/valve/automation.h index f2c06270c07..87e9cde0885 100644 --- a/esphome/components/valve/automation.h +++ b/esphome/components/valve/automation.h @@ -11,7 +11,7 @@ template class OpenAction : public Action { public: explicit OpenAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_open().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_open().perform(); } protected: Valve *valve_; @@ -21,7 +21,7 @@ template class CloseAction : public Action { public: explicit CloseAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_close().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_close().perform(); } protected: Valve *valve_; @@ -31,7 +31,7 @@ template class StopAction : public Action { public: explicit StopAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_stop().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_stop().perform(); } protected: Valve *valve_; @@ -41,7 +41,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Valve *valve) : valve_(valve) {} - void play(Ts... x) override { this->valve_->make_call().set_command_toggle().perform(); } + void play(const Ts &...x) override { this->valve_->make_call().set_command_toggle().perform(); } protected: Valve *valve_; @@ -54,7 +54,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(bool, stop) TEMPLATABLE_VALUE(float, position) - void play(Ts... x) override { + void play(const Ts &...x) override { auto call = this->valve_->make_call(); if (this->stop_.has_value()) call.set_stop(this->stop_.value(x...)); @@ -70,7 +70,7 @@ template class ControlAction : public Action { template class ValveIsOpenCondition : public Condition { public: ValveIsOpenCondition(Valve *valve) : valve_(valve) {} - bool check(Ts... x) override { return this->valve_->is_fully_open(); } + bool check(const Ts &...x) override { return this->valve_->is_fully_open(); } protected: Valve *valve_; @@ -79,7 +79,7 @@ template class ValveIsOpenCondition : public Condition { template class ValveIsClosedCondition : public Condition { public: ValveIsClosedCondition(Valve *valve) : valve_(valve) {} - bool check(Ts... x) override { return this->valve_->is_fully_closed(); } + bool check(const Ts &...x) override { return this->valve_->is_fully_closed(); } protected: Valve *valve_; diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index b041fe84496..381d9061de3 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -1,4 +1,6 @@ #include "valve.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/log.h" #include @@ -147,6 +149,9 @@ void Valve::publish_state(bool save) { ESP_LOGD(TAG, " Current Operation: %s", valve_operation_to_str(this->current_operation)); this->state_callback_.call(); +#if defined(USE_VALVE) && defined(USE_CONTROLLER_REGISTRY) + ControllerRegistry::notify_valve_update(this); +#endif if (save) { ValveRestoreState restore{}; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 7ece73994f3..fd35dc7d09e 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -657,7 +657,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { ESP_LOGW(TAG, "No text in STT_END event"); return; } else if (text.length() > 500) { - text = text.substr(0, 497) + "..."; + text.resize(497); + text += "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); @@ -714,7 +715,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { return; } if (text.length() > 500) { - text = text.substr(0, 497) + "..."; + text.resize(497); + text += "..."; } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 95f77dbf092..8d3d3497ec5 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -324,7 +324,7 @@ template class StartAction : public Action, public Parent TEMPLATABLE_VALUE(std::string, wake_word); public: - void play(Ts... x) override { + void play(const Ts &...x) override { this->parent_->set_wake_word(this->wake_word_.value(x...)); this->parent_->request_start(false, this->silence_detection_); } @@ -337,22 +337,22 @@ template class StartAction : public Action, public Parent template class StartContinuousAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->request_start(true, true); } + void play(const Ts &...x) override { this->parent_->request_start(true, true); } }; template class StopAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->request_stop(); } + void play(const Ts &...x) override { this->parent_->request_stop(); } }; template class IsRunningCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } + bool check(const Ts &...x) override { return this->parent_->is_running() || this->parent_->is_continuous(); } }; template class ConnectedCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->get_api_connection() != nullptr; } + bool check(const Ts &...x) override { return this->parent_->get_api_connection() != nullptr; } }; extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index a7fdf30eef4..17ad496f30d 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -289,6 +289,9 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) + # Track controller registration for StaticVector sizing + CORE.register_controller() + version = config[CONF_VERSION] cg.add(paren.set_port(config[CONF_PORT])) diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 4a98db88776..144d9e76cdb 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -1,17 +1,69 @@ +import logging + import esphome.codegen as cg from esphome.components.esp32 import add_idf_component from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code +from esphome.config_helpers import merge_config import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority +import esphome.final_validate as fv +from esphome.types import ConfigType + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network", "web_server_base"] +CONF_WEB_SERVER = "web_server" + web_server_ns = cg.esphome_ns.namespace("web_server") WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent) + +def _web_server_ota_final_validate(config: ConfigType) -> None: + """Merge multiple web_server OTA instances into one. + + Multiple web_server OTA instances register duplicate HTTP handlers for /update, + causing undefined behavior. Merge them into a single instance. + """ + full_conf = fv.full_config.get() + ota_confs = full_conf.get(CONF_OTA, []) + + web_server_ota_configs: list[ConfigType] = [] + other_ota_configs: list[ConfigType] = [] + + for ota_conf in ota_confs: + if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER: + web_server_ota_configs.append(ota_conf) + else: + other_ota_configs.append(ota_conf) + + if len(web_server_ota_configs) <= 1: + return + + # Merge all web_server OTA configs into the first one + merged = web_server_ota_configs[0] + for ota_conf in web_server_ota_configs[1:]: + # Validate that IDs are consistent if manually specified + if merged[CONF_ID].is_manual and ota_conf[CONF_ID].is_manual: + raise cv.Invalid( + f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent" + ) + merged = merge_config(merged, ota_conf) + + _LOGGER.warning( + "Found and merged %d web_server OTA configurations into one instance", + len(web_server_ota_configs), + ) + + # Replace OTA configs with merged web_server + other OTA platforms + other_ota_configs.append(merged) + full_conf[CONF_OTA] = other_ota_configs + fv.full_config.set(full_conf) + + CONFIG_SCHEMA = ( cv.Schema( { @@ -22,6 +74,8 @@ CONFIG_SCHEMA = ( .extend(cv.COMPONENT_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate + @coroutine_with_priority(CoroPriority.WEB_SERVER_OTA) async def to_code(config): diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1a3dec0e855..5a8128ba431 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -3,6 +3,8 @@ #include "esphome/components/json/json_util.h" #include "esphome/components/network/util.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" +#include "esphome/core/controller_registry.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -294,7 +296,7 @@ std::string WebServer::get_config_json() { } void WebServer::setup() { - this->setup_controller(this->include_internal_); + ControllerRegistry::register_controller(this); this->base_->init(); #ifdef USE_LOGGER @@ -324,7 +326,7 @@ void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:\n" " Address: %s:%u", - network::get_use_address().c_str(), this->base_->get_port()); + network::get_use_address(), this->base_->get_port()); } float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; } @@ -430,7 +432,9 @@ static JsonDetail get_request_detail(AsyncWebServerRequest *request) { } #ifdef USE_SENSOR -void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { +void WebServer::on_sensor_update(sensor::Sensor *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", sensor_state_json_generator); } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -473,7 +477,9 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail #endif #ifdef USE_TEXT_SENSOR -void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { +void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", text_sensor_state_json_generator); } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -513,7 +519,9 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: #endif #ifdef USE_SWITCH -void WebServer::on_switch_update(switch_::Switch *obj, bool state) { +void WebServer::on_switch_update(switch_::Switch *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", switch_state_json_generator); } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -625,6 +633,8 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) #ifdef USE_BINARY_SENSOR void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator); } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -664,6 +674,8 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool #ifdef USE_FAN void WebServer::on_fan_update(fan::Fan *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", fan_state_json_generator); } void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -738,6 +750,8 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { #ifdef USE_LIGHT void WebServer::on_light_update(light::LightState *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", light_state_json_generator); } void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -811,6 +825,8 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi #ifdef USE_COVER void WebServer::on_cover_update(cover::Cover *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", cover_state_json_generator); } void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -895,7 +911,9 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { #endif #ifdef USE_NUMBER -void WebServer::on_number_update(number::Number *obj, float state) { +void WebServer::on_number_update(number::Number *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", number_state_json_generator); } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -961,6 +979,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail #ifdef USE_DATETIME_DATE void WebServer::on_date_update(datetime::DateEntity *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", date_state_json_generator); } void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1016,6 +1036,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_TIME void WebServer::on_time_update(datetime::TimeEntity *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", time_state_json_generator); } void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1070,6 +1092,8 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con #ifdef USE_DATETIME_DATETIME void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", datetime_state_json_generator); } void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1124,7 +1148,9 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s #endif // USE_DATETIME_DATETIME #ifdef USE_TEXT -void WebServer::on_text_update(text::Text *obj, const std::string &state) { +void WebServer::on_text_update(text::Text *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", text_state_json_generator); } void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1178,7 +1204,9 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json #endif #ifdef USE_SELECT -void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { +void WebServer::on_select_update(select::Select *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", select_state_json_generator); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1237,6 +1265,8 @@ std::string WebServer::select_json(select::Select *obj, const char *value, JsonD #ifdef USE_CLIMATE void WebServer::on_climate_update(climate::Climate *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", climate_state_json_generator); } void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1378,6 +1408,8 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf #ifdef USE_LOCK void WebServer::on_lock_update(lock::Lock *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", lock_state_json_generator); } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1449,6 +1481,8 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet #ifdef USE_VALVE void WebServer::on_valve_update(valve::Valve *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", valve_state_json_generator); } void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1530,6 +1564,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) { #ifdef USE_ALARM_CONTROL_PANEL void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", alarm_control_panel_state_json_generator); } void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -1607,7 +1643,9 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro #endif #ifdef USE_EVENT -void WebServer::on_event(event::Event *obj, const std::string &event_type) { +void WebServer::on_event(event::Event *obj) { + if (!this->include_internal_ && obj->is_internal()) + return; this->events_.deferrable_send_state(obj, "state", event_state_json_generator); } @@ -1628,7 +1666,8 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa } static std::string get_event_type(event::Event *event) { - return (event && event->last_event_type) ? *event->last_event_type : ""; + const char *last_type = event ? event->get_last_event_type() : nullptr; + return last_type ? last_type : ""; } std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) { @@ -1649,7 +1688,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty } if (start_config == DETAIL_ALL) { JsonArray event_types = root["event_types"].to(); - for (auto const &event_type : obj->get_event_types()) { + for (const char *event_type : obj->get_event_types()) { event_types.add(event_type); } root["device_class"] = obj->get_device_class_ref(); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 328140cfae9..7e1af886457 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -255,7 +255,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SENSOR - void on_sensor_update(sensor::Sensor *obj, float state) override; + void on_sensor_update(sensor::Sensor *obj) override; /// Handle a sensor request under '/sensor/'. void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -266,7 +266,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SWITCH - void on_switch_update(switch_::Switch *obj, bool state) override; + void on_switch_update(switch_::Switch *obj) override; /// Handle a switch request under '/switch//'. void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -324,7 +324,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_TEXT_SENSOR - void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; + void on_text_sensor_update(text_sensor::TextSensor *obj) override; /// Handle a text sensor request under '/text_sensor/'. void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -348,7 +348,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_NUMBER - void on_number_update(number::Number *obj, float state) override; + void on_number_update(number::Number *obj) override; /// Handle a number request under '/number/'. void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -392,7 +392,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_TEXT - void on_text_update(text::Text *obj, const std::string &state) override; + void on_text_update(text::Text *obj) override; /// Handle a text input request under '/text/'. void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -403,7 +403,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state, size_t index) override; + void on_select_update(select::Select *obj) override; /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); @@ -462,7 +462,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_EVENT - void on_event(event::Event *obj, const std::string &event_type) override; + void on_event(event::Event *obj) override; static std::string event_state_json_generator(WebServer *web_server, void *source); static std::string event_all_json_generator(WebServer *web_server, void *source); diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b980bab4aa8..11bd7798e27 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,12 +1,17 @@ +import logging + from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components.const import CONF_USE_PSRAM from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant -from esphome.components.network import ip_address_literal +from esphome.components.network import ( + has_high_performance_networking, + ip_address_literal, +) +from esphome.components.psram import is_guaranteed as psram_is_guaranteed from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv -from esphome.config_validation import only_with_esp_idf from esphome.const import ( CONF_AP, CONF_BSSID, @@ -42,6 +47,7 @@ from esphome.const import ( CONF_TTLS_PHASE_2, CONF_USE_ADDRESS, CONF_USERNAME, + Platform, PlatformFramework, ) from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority @@ -49,10 +55,19 @@ import esphome.final_validate as fv from . import wpa2_eap +_LOGGER = logging.getLogger(__name__) + AUTO_LOAD = ["network"] +_LOGGER = logging.getLogger(__name__) + NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +CONF_MIN_AUTH_MODE = "min_auth_mode" + +# Maximum number of WiFi networks that can be configured +# Limited to 127 because selected_sta_index_ is int8_t in C++ +MAX_WIFI_NETWORKS = 127 wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") @@ -66,6 +81,14 @@ WIFI_POWER_SAVE_MODES = { "LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT, "HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH, } + +WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode") +WIFI_MIN_AUTH_MODES = { + "WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA, + "WPA2": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2, + "WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3, +} +VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True) WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition) WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) @@ -170,7 +193,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( { cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, - cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, + cv.Optional(CONF_PRIORITY, default=0): cv.int_range(min=-128, max=127), cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, } ) @@ -183,6 +206,27 @@ def validate_variant(_): raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") +def _apply_min_auth_mode_default(config): + """Apply platform-specific default for min_auth_mode and warn ESP8266 users.""" + # Only apply defaults for platforms that support min_auth_mode + if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32): + if CORE.is_esp8266: + _LOGGER.warning( + "The minimum WiFi authentication mode (wifi -> min_auth_mode) is not set. " + "This controls the weakest encryption your device will accept when connecting to WiFi. " + "Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. " + "WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. " + "WPA2 uses AES encryption which is significantly more secure. " + "To silence this warning, explicitly set min_auth_mode under 'wifi:'. " + "If your router supports WPA2 or WPA3, set 'min_auth_mode: WPA2'. " + "If your router only supports WPA, set 'min_auth_mode: WPA'." + ) + config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA") + elif CORE.is_esp32: + config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA2") + return config + + def final_validate(config): has_sta = bool(config.get(CONF_NETWORKS, True)) has_ap = CONF_AP in config @@ -260,7 +304,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WiFiComponent), - cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), + cv.Optional(CONF_NETWORKS): cv.All( + cv.ensure_list(WIFI_NETWORK_STA), cv.Length(max=MAX_WIFI_NETWORKS) + ), cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, @@ -281,6 +327,10 @@ CONFIG_SCHEMA = cv.All( ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, + cv.Optional(CONF_MIN_AUTH_MODE): cv.All( + VALIDATE_WIFI_MIN_AUTH_MODE, + cv.only_on([Platform.ESP32, Platform.ESP8266]), + ), cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( cv.decibel, cv.float_range(min=8.5, max=20.5) ), @@ -301,10 +351,11 @@ CONFIG_SCHEMA = cv.All( single=True ), cv.Optional(CONF_USE_PSRAM): cv.All( - only_with_esp_idf, cv.requires_component("psram"), cv.boolean + cv.only_on_esp32, cv.requires_component("psram"), cv.boolean ), } ), + _apply_min_auth_mode_default, _validate, ) @@ -379,6 +430,8 @@ async def to_code(config): # Track if any network uses Enterprise authentication has_eap = False + # Track if any network uses manual IP + has_manual_ip = False # Initialize FixedVector with the count of networks networks = config.get(CONF_NETWORKS, []) @@ -392,11 +445,15 @@ async def to_code(config): for network in networks: if CONF_EAP in network: has_eap = True + if network.get(CONF_MANUAL_IP) or config.get(CONF_MANUAL_IP): + has_manual_ip = True cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) if CONF_AP in config: conf = config[CONF_AP] ip_config = conf.get(CONF_MANUAL_IP) + if ip_config: + has_manual_ip = True cg.with_local_variable( conf[CONF_ID], WiFiAP(), @@ -412,8 +469,14 @@ async def to_code(config): if CORE.is_esp32: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap) + # Only define USE_WIFI_MANUAL_IP if any AP uses manual IP + if has_manual_ip: + cg.add_define("USE_WIFI_MANUAL_IP") + cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) + if CONF_MIN_AUTH_MODE in config: + cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) if config[CONF_FAST_CONNECT]: cg.add_define("USE_WIFI_FAST_CONNECT") cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) @@ -438,6 +501,56 @@ async def to_code(config): if config.get(CONF_USE_PSRAM): add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) + + # Apply high performance WiFi settings if high performance networking is enabled + if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking(): + # Check if PSRAM is guaranteed (set by psram component during final validation) + psram_guaranteed = psram_is_guaranteed() + + # Always allocate WiFi buffers in PSRAM if available + add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True) + + if psram_guaranteed: + _LOGGER.info( + "Applying high-performance WiFi settings (PSRAM guaranteed): 512 RX buffers, 32 TX buffers" + ) + # PSRAM is guaranteed - use aggressive settings + # Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true in networking component + # Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702 + + # Large dynamic RX buffers (requires PSRAM) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 512) + + # Static TX buffers for better performance + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BUFFER_TYPE", 0) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM", 32) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM", 8) + + # AMPDU settings optimized for PSRAM + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) + else: + _LOGGER.info( + "Applying optimized WiFi settings: 64 RX buffers, 64 TX buffers" + ) + # PSRAM not guaranteed - use more conservative, but still optimized settings + # Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32 + + # Standard buffer counts + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64) + + # Standard AMPDU settings + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True) + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32) + cg.add_define("USE_WIFI") # must register before OTA safe mode check diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index b278e5a386e..859a73afdbd 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" #ifdef USE_WIFI +#include #include #ifdef USE_ESP32 @@ -41,6 +42,264 @@ namespace wifi { static const char *const TAG = "wifi"; +/// WiFi Retry Logic - Priority-Based BSSID Selection +/// +/// The WiFi component uses a state machine with priority degradation to handle connection failures +/// and automatically cycle through different BSSIDs in mesh networks or multiple configured networks. +/// +/// Connection Flow: +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Fast Connect Path (Optional) │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ Entered if: configuration has 'fast_connect: true' │ +/// │ Optimization to skip scanning when possible: │ +/// │ │ +/// │ 1. INITIAL_CONNECT → Try one of: │ +/// │ a) Saved BSSID+channel (from previous boot) │ +/// │ b) First configured non-hidden network (any BSSID) │ +/// │ ↓ │ +/// │ [FAILED] → Check if more configured networks available │ +/// │ ↓ │ +/// │ 2. FAST_CONNECT_CYCLING_APS → Try remaining configured networks │ +/// │ (1 attempt each, any BSSID) │ +/// │ ↓ │ +/// │ [All Failed] → Fall through to explicit hidden or scanning │ +/// │ │ +/// │ Note: Fast connect data saved from previous successful connection │ +/// └──────────────────────────────────────────────────────────────────────┘ +/// ↓ +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Explicit Hidden Networks Path (Optional) │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ Entered if: first configured network has 'hidden: true' │ +/// │ │ +/// │ 1. EXPLICIT_HIDDEN → Try consecutive hidden networks (1 attempt) │ +/// │ Stop when visible network reached │ +/// │ ↓ │ +/// │ Example: Hidden1, Hidden2, Visible1, Hidden3, Visible2 │ +/// │ Try: Hidden1, Hidden2 (stop at Visible1) │ +/// │ ↓ │ +/// │ [All Failed] → Fall back to scan-based connection │ +/// │ │ +/// │ Note: Fast connect saves BSSID after first successful connection, │ +/// │ so subsequent boots use fast path instead of hidden mode │ +/// └──────────────────────────────────────────────────────────────────────┘ +/// ↓ +/// ┌──────────────────────────────────────────────────────────────────────┐ +/// │ Scan-Based Connection Path │ +/// ├──────────────────────────────────────────────────────────────────────┤ +/// │ │ +/// │ 1. SCAN → Sort by priority (highest first), then RSSI │ +/// │ ┌─────────────────────────────────────────────────┐ │ +/// │ │ scan_result_[0] = Best BSSID (highest priority) │ │ +/// │ │ scan_result_[1] = Second best │ │ +/// │ │ scan_result_[2] = Third best │ │ +/// │ └─────────────────────────────────────────────────┘ │ +/// │ ↓ │ +/// │ 2. SCAN_CONNECTING → Try scan_result_[0] (2 attempts) │ +/// │ (Visible1, Visible2 from example above) │ +/// │ ↓ │ +/// │ 3. FAILED → Decrease priority: 0.0 → -1.0 → -2.0 │ +/// │ (stored in persistent sta_priorities_) │ +/// │ ↓ │ +/// │ 4. Check for hidden networks: │ +/// │ - If found → RETRY_HIDDEN (try SSIDs not in scan, 1 attempt) │ +/// │ Skip hidden networks before first visible one │ +/// │ (Skip Hidden1/Hidden2, try Hidden3 from example) │ +/// │ - If none → Skip RETRY_HIDDEN, go to step 5 │ +/// │ ↓ │ +/// │ 5. FAILED → RESTARTING_ADAPTER (skipped if AP/improv active) │ +/// │ ↓ │ +/// │ 6. Loop back to start: │ +/// │ - If first network is hidden → EXPLICIT_HIDDEN (retry cycle) │ +/// │ - Otherwise → SCAN_CONNECTING (rescan) │ +/// │ ↓ │ +/// │ 7. RESCAN → Apply stored priorities, sort again │ +/// │ ┌─────────────────────────────────────────────────┐ │ +/// │ │ scan_result_[0] = BSSID B (priority 0.0) ← NEW │ │ +/// │ │ scan_result_[1] = BSSID C (priority 0.0) │ │ +/// │ │ scan_result_[2] = BSSID A (priority -2.0) ← OLD │ │ +/// │ └─────────────────────────────────────────────────┘ │ +/// │ ↓ │ +/// │ 8. SCAN_CONNECTING → Try scan_result_[0] (next best) │ +/// │ │ +/// │ Key: Priority system cycles through BSSIDs ACROSS scan cycles │ +/// │ Full retry cycle: EXPLICIT_HIDDEN → SCAN → RETRY_HIDDEN │ +/// │ Always try best available BSSID (scan_result_[0]) │ +/// └──────────────────────────────────────────────────────────────────────┘ +/// +/// Retry Phases: +/// - INITIAL_CONNECT: Try saved BSSID+channel (fast_connect), or fall back to normal flow +/// - FAST_CONNECT_CYCLING_APS: Cycle through remaining configured networks (1 attempt each, fast_connect only) +/// - EXPLICIT_HIDDEN: Try consecutive networks marked hidden:true before scanning (1 attempt per SSID) +/// - SCAN_CONNECTING: Connect using scan results (2 attempts per BSSID) +/// - RETRY_HIDDEN: Try networks not found in scan (1 attempt per SSID, skipped if none found) +/// - RESTARTING_ADAPTER: Restart WiFi adapter to clear stuck state +/// +/// Hidden Network Handling: +/// - Networks marked 'hidden: true' before first non-hidden → Tried in EXPLICIT_HIDDEN phase +/// - Networks marked 'hidden: true' after first non-hidden → Tried in RETRY_HIDDEN phase +/// - After successful connection, fast_connect saves BSSID → subsequent boots use fast path +/// - Networks not in scan results → Tried in RETRY_HIDDEN phase +/// - Networks visible in scan + not marked hidden → Skipped in RETRY_HIDDEN phase +/// - Networks marked 'hidden: true' always use hidden mode, even if broadcasting SSID + +static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { + switch (phase) { + case WiFiRetryPhase::INITIAL_CONNECT: + return LOG_STR("INITIAL_CONNECT"); +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + return LOG_STR("FAST_CONNECT_CYCLING"); +#endif + case WiFiRetryPhase::EXPLICIT_HIDDEN: + return LOG_STR("EXPLICIT_HIDDEN"); + case WiFiRetryPhase::SCAN_CONNECTING: + return LOG_STR("SCAN_CONNECTING"); + case WiFiRetryPhase::RETRY_HIDDEN: + return LOG_STR("RETRY_HIDDEN"); + case WiFiRetryPhase::RESTARTING_ADAPTER: + return LOG_STR("RESTARTING"); + default: + return LOG_STR("UNKNOWN"); + } +} + +bool WiFiComponent::went_through_explicit_hidden_phase_() const { + // If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase + // This means those networks were already tried and should be skipped in RETRY_HIDDEN + return !this->sta_.empty() && this->sta_[0].get_hidden(); +} + +int8_t WiFiComponent::find_first_non_hidden_index_() const { + // Find the first network that is NOT marked hidden:true + // This is where EXPLICIT_HIDDEN phase would have stopped + for (size_t i = 0; i < this->sta_.size(); i++) { + if (!this->sta_[i].get_hidden()) { + return static_cast(i); + } + } + return -1; // All networks are hidden +} + +// 2 attempts per BSSID in SCAN_CONNECTING phase +// Rationale: This is the ONLY phase where we decrease BSSID priority, so we must be very sure. +// Auth failures are common immediately after scan due to WiFi stack state transitions. +// Trying twice filters out false positives and prevents unnecessarily marking a good BSSID as bad. +// After 2 genuine failures, priority degradation ensures we skip this BSSID on subsequent scans. +static constexpr uint8_t WIFI_RETRY_COUNT_PER_BSSID = 2; + +// 1 attempt per SSID in RETRY_HIDDEN phase +// Rationale: Try hidden mode once, then rescan to get next best BSSID via priority system +static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1; + +// 1 attempt per AP in fast_connect mode (INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS) +// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly +static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1; + +/// Cooldown duration in milliseconds after adapter restart or repeated failures +/// Allows WiFi hardware to stabilize before next connection attempt +static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000; + +static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) { + switch (phase) { + case WiFiRetryPhase::INITIAL_CONNECT: +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: +#endif + // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS both use 1 attempt per AP (fast_connect mode) + return WIFI_RETRY_COUNT_PER_AP; + case WiFiRetryPhase::EXPLICIT_HIDDEN: + // Explicitly hidden network: 1 attempt (user marked as hidden, try once then scan) + return WIFI_RETRY_COUNT_PER_SSID; + case WiFiRetryPhase::SCAN_CONNECTING: + // Scan-based phase: 2 attempts per BSSID (handles transient auth failures after scan) + return WIFI_RETRY_COUNT_PER_BSSID; + case WiFiRetryPhase::RETRY_HIDDEN: + // Hidden network mode: 1 attempt per SSID + return WIFI_RETRY_COUNT_PER_SSID; + default: + return WIFI_RETRY_COUNT_PER_BSSID; + } +} + +static void apply_scan_result_to_params(WiFiAP ¶ms, const WiFiScanResult &scan) { + params.set_hidden(false); + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); +} + +bool WiFiComponent::needs_scan_results_() const { + // Only SCAN_CONNECTING phase needs scan results + if (this->retry_phase_ != WiFiRetryPhase::SCAN_CONNECTING) { + return false; + } + // Need scan if we have no results or no matching networks + return this->scan_result_.empty() || !this->scan_result_[0].get_matches(); +} + +bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const { + // Check if this SSID is configured as hidden + // If explicitly marked hidden, we should always try hidden mode regardless of scan results + for (const auto &conf : this->sta_) { + if (conf.get_ssid() == ssid && conf.get_hidden()) { + return false; // Treat as not seen - force hidden mode attempt + } + } + + // Otherwise, check if we saw it in scan results + for (const auto &scan : this->scan_result_) { + if (scan.get_ssid() == ssid) { + return true; + } + } + return false; +} + +int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { + // Find next SSID that wasn't in scan results (might be hidden) + bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_(); + // Start searching from start_index + 1 + for (size_t i = start_index + 1; i < this->sta_.size(); i++) { + const auto &sta = this->sta_[i]; + + // Skip networks that were already tried in EXPLICIT_HIDDEN phase + // Those are: networks marked hidden:true that appear before the first non-hidden network + // If all networks are hidden (first_non_hidden_idx == -1), skip all of them + if (!include_explicit_hidden && sta.get_hidden()) { + int8_t first_non_hidden_idx = this->find_first_non_hidden_index_(); + if (first_non_hidden_idx < 0 || static_cast(i) < first_non_hidden_idx) { + ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str()); + continue; + } + } + + if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) { + ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast(i)); + return static_cast(i); + } + ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str()); + } + // No hidden SSIDs found + return -1; +} + +void WiFiComponent::start_initial_connection_() { + // If first network (highest priority) is explicitly marked hidden, try it first before scanning + // This respects user's priority order when they explicitly configure hidden networks + if (!this->sta_.empty() && this->sta_[0].get_hidden()) { + ESP_LOGI(TAG, "Starting with explicit hidden network (highest priority)"); + this->selected_sta_index_ = 0; + this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN; + WiFiAP params = this->build_params_for_current_phase_(); + this->start_connecting(params); + } else { + ESP_LOGI(TAG, "Starting scan"); + this->start_scanning(); + } +} + #if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) { switch (type) { @@ -108,15 +367,28 @@ void WiFiComponent::start() { ESP_LOGV(TAG, "Setting Power Save Option failed"); } + this->transition_to_phase_(WiFiRetryPhase::INITIAL_CONNECT); #ifdef USE_WIFI_FAST_CONNECT - this->trying_loaded_ap_ = this->load_fast_connect_settings_(); - if (!this->trying_loaded_ap_) { - this->ap_index_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; + WiFiAP params; + bool loaded_fast_connect = this->load_fast_connect_settings_(params); + // Fast connect optimization: only use when we have saved BSSID+channel data + // Without saved data, try first configured network or use normal flow + if (loaded_fast_connect) { + ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str()); + this->start_connecting(params); + } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) { + // No saved data, but have configured networks - try first non-hidden network + ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str()); + this->selected_sta_index_ = 0; + params = this->build_params_for_current_phase_(); + this->start_connecting(params); + } else { + // No saved data and (no networks OR first is hidden) - use normal flow + this->start_initial_connection_(); } - this->start_connecting(this->selected_ap_, false); #else - this->start_scanning(); + // Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden) + this->start_initial_connection_(); #endif #ifdef USE_WIFI_AP } else if (this->has_ap()) { @@ -145,9 +417,11 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - delay(100); // NOLINT - this->num_retried_ = 0; - this->retry_hidden_ = false; + // Enter cooldown state to allow WiFi hardware to stabilize after restart + // Don't set retry_phase_ or num_retried_ here - state machine handles transitions + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; + this->action_started_ = millis(); + this->error_from_callback_ = false; } void WiFiComponent::loop() { @@ -167,21 +441,12 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { this->status_set_warning(LOG_STR("waiting to reconnect")); - if (millis() - this->action_started_ > 5000) { -#ifdef USE_WIFI_FAST_CONNECT - // NOTE: This check may not make sense here as it could interfere with AP cycling - if (!this->selected_ap_.get_bssid().has_value()) - this->selected_ap_ = this->sta_[0]; - this->start_connecting(this->selected_ap_, false); -#else - if (this->retry_hidden_) { - if (!this->selected_ap_.get_bssid().has_value()) - this->selected_ap_ = this->sta_[0]; - this->start_connecting(this->selected_ap_, false); - } else { - this->start_scanning(); - } -#endif + if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) { + // After cooldown we either restarted the adapter because of + // a failure, or something tried to connect over and over + // so we entered cooldown. In both cases we call + // check_connecting_finished to continue the state machine. + this->check_connecting_finished(); } break; } @@ -190,8 +455,7 @@ void WiFiComponent::loop() { this->check_scanning_finished(); break; } - case WIFI_COMPONENT_STATE_STA_CONNECTING: - case WIFI_COMPONENT_STATE_STA_CONNECTING_2: { + case WIFI_COMPONENT_STATE_STA_CONNECTING: { this->status_set_warning(LOG_STR("associating to network")); this->check_connecting_finished(); break; @@ -201,6 +465,8 @@ void WiFiComponent::loop() { if (!this->is_connected()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + // Clear error flag before reconnecting so first attempt is not seen as immediate failure + this->error_from_callback_ = false; this->retry_connect(); } else { this->status_clear_warning(); @@ -273,8 +539,8 @@ network::IPAddress WiFiComponent::get_dns_address(int num) { } // set_use_address() is guaranteed to be called during component setup by Python code generation, // so use_address_ will always be valid when get_use_address() is called - no fallback needed. -const std::string &WiFiComponent::get_use_address() const { return this->use_address_; } -void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } +const char *WiFiComponent::get_use_address() const { return this->use_address_; } +void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } #ifdef USE_WIFI_AP void WiFiComponent::setup_ap_config_() { @@ -305,6 +571,7 @@ void WiFiComponent::setup_ap_config_() { " IP Address: %s", this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str()); +#ifdef USE_WIFI_MANUAL_IP auto manual_ip = this->ap_.get_manual_ip(); if (manual_ip.has_value()) { ESP_LOGCONFIG(TAG, @@ -314,6 +581,7 @@ void WiFiComponent::setup_ap_config_() { manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(), manual_ip->subnet.str().c_str()); } +#endif if (!this->has_sta()) { this->state_ = WIFI_COMPONENT_STATE_AP; @@ -336,8 +604,56 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); this->init_sta(1); this->add_sta(ap); + this->selected_sta_index_ = 0; +} + +WiFiAP WiFiComponent::build_params_for_current_phase_() { + const WiFiAP *config = this->get_selected_sta_(); + if (config == nullptr) { + ESP_LOGE(TAG, "No valid network config (selected_sta_index_=%d, sta_.size()=%zu)", + static_cast(this->selected_sta_index_), this->sta_.size()); + // Return empty params - caller should handle this gracefully + return WiFiAP(); + } + + WiFiAP params = *config; + + switch (this->retry_phase_) { + case WiFiRetryPhase::INITIAL_CONNECT: +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: +#endif + // Fast connect phases: use config-only (no scan results) + // BSSID/channel from config if user specified them, otherwise empty + break; + + case WiFiRetryPhase::EXPLICIT_HIDDEN: + case WiFiRetryPhase::RETRY_HIDDEN: + // Hidden network mode: clear BSSID/channel to trigger probe request + // (both explicit hidden and retry hidden use same behavior) + params.set_bssid(optional{}); + params.set_channel(optional{}); + break; + + case WiFiRetryPhase::SCAN_CONNECTING: + // Scan-based phase: always use best scan result (index 0 - highest priority after sorting) + if (!this->scan_result_.empty()) { + apply_scan_result_to_params(params, this->scan_result_[0]); + } + break; + + case WiFiRetryPhase::RESTARTING_ADAPTER: + // Should not be building params during restart + break; + } + + return params; +} + +WiFiAP WiFiComponent::get_sta() const { + const WiFiAP *config = this->get_selected_sta_(); + return config ? *config : WiFiAP{}; } -void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 @@ -352,13 +668,27 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa this->set_sta(sta); } -void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { - ESP_LOGI(TAG, "Connecting to '%s'", ap.get_ssid().c_str()); +void WiFiComponent::start_connecting(const WiFiAP &ap) { + // Log connection attempt at INFO level with priority + char bssid_s[18]; + int8_t priority = 0; + + if (ap.get_bssid().has_value()) { + format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s); + priority = this->get_sta_priority(ap.get_bssid().value()); + } + + ESP_LOGI(TAG, + "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...", + ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority, + this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_), + LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); + #ifdef ESPHOME_LOG_HAS_VERBOSE ESP_LOGV(TAG, "Connection Params:"); ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str()); if (ap.get_bssid().has_value()) { - ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str()); + ESP_LOGV(TAG, " BSSID: %s", bssid_s); } else { ESP_LOGV(TAG, " BSSID: Not Set"); } @@ -390,11 +720,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } else { ESP_LOGV(TAG, " Channel not set"); } +#ifdef USE_WIFI_MANUAL_IP if (ap.get_manual_ip().has_value()) { ManualIP m = *ap.get_manual_ip(); ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(), m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str()); - } else { + } else +#endif + { ESP_LOGV(TAG, " Using DHCP IP"); } ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); @@ -402,19 +735,24 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { if (!this->wifi_sta_connect_(ap)) { ESP_LOGE(TAG, "wifi_sta_connect_ failed"); - this->retry_connect(); - return; - } - - if (!two) { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; + // Enter cooldown to allow WiFi hardware to stabilize + // (immediate failure suggests hardware not ready, different from connection timeout) + this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; } else { - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; } this->action_started_ = millis(); } const LogString *get_signal_bars(int8_t rssi) { + // Check for disconnected sentinel value first + if (rssi == WIFI_RSSI_DISCONNECTED) { + // MULTIPLICATION SIGN + // Unicode: U+00D7, UTF-8: C3 97 + return LOG_STR("\033[0;31m" // red + "\xc3\x97\xc3\x97\xc3\x97\xc3\x97" + "\033[0m"); + } // LOWER ONE QUARTER BLOCK // Unicode: U+2582, UTF-8: E2 96 82 // LOWER HALF BLOCK @@ -459,6 +797,8 @@ const LogString *get_signal_bars(int8_t rssi) { void WiFiComponent::print_connect_params_() { bssid_t bssid = wifi_bssid(); + char bssid_s[18]; + format_mac_addr_upper(bssid.data(), bssid_s); ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str()); if (this->is_disabled()) { @@ -481,12 +821,12 @@ void WiFiComponent::print_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi, - LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), - wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); + wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), + get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), + wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (this->selected_ap_.get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid())); } #endif #ifdef USE_WIFI_11KV_SUPPORT @@ -526,8 +866,39 @@ void WiFiComponent::start_scanning() { this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; } -// Helper function for WiFi scan result comparison -// Returns true if 'a' should be placed before 'b' in the sorted order +/// Comparator for WiFi scan result sorting - determines which network should be tried first +/// Returns true if 'a' should be placed before 'b' in the sorted order (a is "better" than b) +/// +/// Sorting logic (in priority order): +/// 1. Matching networks always ranked before non-matching networks +/// 2. For matching networks: Priority first (CRITICAL - tracks failure history) +/// 3. RSSI as tiebreaker for equal priority or non-matching networks +/// +/// WHY PRIORITY MUST BE CHECKED FIRST: +/// The priority field tracks connection failure history via priority degradation: +/// - Initial priority: 0.0 (from config or default) +/// - Each connection failure: priority -= 1.0 (becomes -1.0, -2.0, -3.0, etc.) +/// - Failed BSSIDs sorted lower → naturally try different BSSID on next scan +/// +/// This enables automatic BSSID cycling for various real-world failure scenarios: +/// - Crashed/hung AP (visible but not responding) +/// - Misconfigured mesh node (accepts auth but no DHCP/routing) +/// - Capacity limits (AP refuses new clients) +/// - Rogue AP (same SSID, wrong password or malicious) +/// - Intermittent hardware issues (flaky radio, overheating) +/// +/// Example mesh network: 3 APs with same SSID "home", all at priority 0.0 initially +/// - Try strongest BSSID A (sorted by RSSI) → fails → priority A becomes -1.0 +/// - Next scan: BSSID B and C (priority 0.0) sorted BEFORE A (priority -1.0) +/// - Try next strongest BSSID B → succeeds or fails and gets deprioritized +/// - System naturally cycles through all BSSIDs via priority degradation +/// - Eventually finds working AP or tries all options before restarting adapter +/// +/// If we checked RSSI first (Bug in PR #9963): +/// - Same failed BSSID would keep being selected if it has strongest signal +/// - Device stuck connecting to crashed AP with -30dBm while working AP at -50dBm ignored +/// - Priority degradation would be useless +/// - Mesh networks would never recover from single AP failure [[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) { // Matching networks always come before non-matching if (a.get_matches() && !b.get_matches()) @@ -535,21 +906,13 @@ void WiFiComponent::start_scanning() { if (!a.get_matches() && b.get_matches()) return false; - if (a.get_matches() && b.get_matches()) { - // For APs with the same SSID, always prefer stronger signal - // This helps with mesh networks and multiple APs - if (a.get_ssid() == b.get_ssid()) { - return a.get_rssi() > b.get_rssi(); - } - - // For different SSIDs, check priority first - if (a.get_priority() != b.get_priority()) - return a.get_priority() > b.get_priority(); - // If priorities are equal, prefer stronger signal - return a.get_rssi() > b.get_rssi(); + // Both matching: check priority first (tracks connection failures via priority degradation) + // Priority is decreased when a BSSID fails to connect, so lower priority = previously failed + if (a.get_matches() && b.get_matches() && a.get_priority() != b.get_priority()) { + return a.get_priority() > b.get_priority(); } - // Both don't match - sort by signal strength + // Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks) return a.get_rssi() > b.get_rssi(); } @@ -584,10 +947,7 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); - ESP_LOGD(TAG, - " Channel: %u\n" - " RSSI: %d dB", - res.get_channel(), res.get_rssi()); + ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority()); } else { ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi()))); @@ -633,59 +993,51 @@ void WiFiComponent::check_scanning_finished() { log_scan_result(res); } - if (!this->scan_result_[0].get_matches()) { - ESP_LOGW(TAG, "No matching network found"); - this->retry_connect(); - return; + // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ + // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config + // matches that network and record it in selected_sta_index_. This keeps the two indices + // synchronized so build_params_for_current_phase_() can safely use both to build connection parameters. + const WiFiScanResult &scan_res = this->scan_result_[0]; + bool found_match = false; + if (scan_res.get_matches()) { + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // No overflow check needed - YAML validation prevents >127 networks + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] + found_match = true; + break; + } + } } - // Build connection params directly into selected_ap_ to avoid extra copy - const WiFiScanResult &scan_res = this->scan_result_[0]; - WiFiAP &selected = this->selected_ap_; - for (auto &config : this->sta_) { - // search for matching STA config, at least one will match (from checks before) - if (!scan_res.matches(config)) { - continue; + if (!found_match) { + ESP_LOGW(TAG, "No matching network found"); + // No scan results matched our configured networks - transition directly to hidden mode + // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize) + this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN); + // If no hidden networks to try, skip connection attempt (will be handled on next loop) + if (this->selected_sta_index_ == -1) { + return; } - - if (config.get_hidden()) { - // selected network is hidden, we use the data from the config - selected.set_hidden(true); - selected.set_ssid(config.get_ssid()); - // Clear channel and BSSID for hidden networks - there might be multiple hidden networks - // but we can't know which one is the correct one. Rely on probe-req with just SSID. - selected.set_channel(0); - selected.set_bssid(optional{}); - } else { - // selected network is visible, we use the data from the scan - // limit the connect params to only connect to exactly this network - // (network selection is done during scan phase). - selected.set_hidden(false); - selected.set_ssid(scan_res.get_ssid()); - selected.set_channel(scan_res.get_channel()); - selected.set_bssid(scan_res.get_bssid()); - } - // copy manual IP (if set) - selected.set_manual_ip(config.get_manual_ip()); - -#ifdef USE_WIFI_WPA2_EAP - // copy EAP parameters (if set) - selected.set_eap(config.get_eap()); -#endif - - // copy password (if set) - selected.set_password(config.get_password()); - - break; + // Now start connection attempt in hidden mode + } else if (this->transition_to_phase_(WiFiRetryPhase::SCAN_CONNECTING)) { + return; // scan started, wait for next loop iteration } yield(); - this->start_connecting(this->selected_ap_, false); + WiFiAP params = this->build_params_for_current_phase_(); + // Ensure we're in SCAN_CONNECTING phase when connecting with scan results + // (needed when scan was started directly without transition_to_phase_, e.g., initial scan) + this->start_connecting(params); } void WiFiComponent::dump_config() { - ESP_LOGCONFIG(TAG, "WiFi:"); + ESP_LOGCONFIG(TAG, + "WiFi:\n" + " Connected: %s", + YESNO(this->is_connected())); this->print_connect_params_(); } @@ -700,10 +1052,20 @@ void WiFiComponent::check_connecting_finished() { } ESP_LOGI(TAG, "Connected"); - // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_ && !this->selected_ap_.get_hidden()) - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", this->selected_ap_.get_ssid().c_str()); - this->retry_hidden_ = false; + // Warn if we had to retry with hidden network mode for a network that's not marked hidden + // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry + if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN && + config && !config->get_hidden() && + this->scan_result_.empty()) { + ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str()); + } + // Reset to initial phase on successful connection (don't log transition, just reset state) + this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; + this->num_retried_ = 0; + // Ensure next connection attempt does not inherit error state + // so when WiFi disconnects later we start fresh and don't see + // the first connection as a failure. + this->error_from_callback_ = false; this->print_connect_params_(); @@ -725,16 +1087,19 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; + // Clear priority tracking if all priorities are at minimum + this->clear_priorities_if_all_min_(); + +#ifdef USE_WIFI_FAST_CONNECT + this->save_fast_connect_settings_(); +#endif + // Free scan results memory unless a component needs them if (!this->keep_scan_results_) { this->scan_result_.clear(); this->scan_result_.shrink_to_fit(); } -#ifdef USE_WIFI_FAST_CONNECT - this->save_fast_connect_settings_(); -#endif - return; } @@ -746,7 +1111,7 @@ void WiFiComponent::check_connecting_finished() { } if (this->error_from_callback_) { - ESP_LOGW(TAG, "Connecting to network failed"); + ESP_LOGW(TAG, "Connecting to network failed (callback)"); this->retry_connect(); return; } @@ -771,62 +1136,390 @@ void WiFiComponent::check_connecting_finished() { this->retry_connect(); } -void WiFiComponent::retry_connect() { - if (this->selected_ap_.get_bssid()) { - auto bssid = *this->selected_ap_.get_bssid(); - float priority = this->get_sta_priority(bssid); - this->set_sta_priority(bssid, priority - 1.0f); +/// Determine the next retry phase based on current state and failure conditions +/// This function examines the current retry phase, number of retries, and failure reasons +/// to decide what phase to move to next. It does not modify any state - it only returns +/// the recommended next phase. +/// +/// @return The next WiFiRetryPhase to transition to (may be same as current phase if should retry) +WiFiRetryPhase WiFiComponent::determine_next_phase_() { + switch (this->retry_phase_) { + case WiFiRetryPhase::INITIAL_CONNECT: +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan + if (this->selected_sta_index_ < static_cast(this->sta_.size()) - 1) { + return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP + } +#endif + // Check if we should try explicit hidden networks before scanning + // This handles reconnection after connection loss where first network is hidden + if (!this->sta_.empty() && this->sta_[0].get_hidden()) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + // No more APs to try, fall back to scan + return WiFiRetryPhase::SCAN_CONNECTING; + + case WiFiRetryPhase::EXPLICIT_HIDDEN: { + // Try all explicitly hidden networks before scanning + if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) { + return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID + } + + // Exhausted retries on current SSID - check for more explicitly hidden networks + // Stop when we reach a visible network (proceed to scanning) + size_t next_index = this->selected_sta_index_ + 1; + if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) { + // Found another explicitly hidden network + return WiFiRetryPhase::EXPLICIT_HIDDEN; + } + + // No more consecutive explicitly hidden networks + // If ALL networks are hidden, skip scanning and go directly to restart + if (this->find_first_non_hidden_index_() < 0) { + return WiFiRetryPhase::RESTARTING_ADAPTER; + } + // Otherwise proceed to scanning for non-hidden networks + return WiFiRetryPhase::SCAN_CONNECTING; + } + + case WiFiRetryPhase::SCAN_CONNECTING: + // If scan found no matching networks, skip to hidden network mode + if (!this->scan_result_.empty() && !this->scan_result_[0].get_matches()) { + return WiFiRetryPhase::RETRY_HIDDEN; + } + + if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) { + return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID + } + + // Exhausted retries on current BSSID (scan_result_[0]) + // Its priority has been decreased, so on next scan it will be sorted lower + // and we'll try the next best BSSID. + // Check if there are any potentially hidden networks to try + if (this->find_next_hidden_sta_(-1) >= 0) { + return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try + } + // No hidden networks - always go through RESTARTING_ADAPTER phase + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active + return WiFiRetryPhase::RESTARTING_ADAPTER; + + case WiFiRetryPhase::RETRY_HIDDEN: + // If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan + if (this->selected_sta_index_ >= 0) { + if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) { + return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID + } + + // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try + if (this->selected_sta_index_ < static_cast(this->sta_.size()) - 1) { + // Check if find_next_hidden_sta_() would actually find another hidden SSID + // as it might have been seen in the scan results and we want to skip those + // otherwise we will get stuck in RETRY_HIDDEN phase + if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) { + // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect() + return WiFiRetryPhase::RETRY_HIDDEN; + } + } + } + // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active + return WiFiRetryPhase::RESTARTING_ADAPTER; + + case WiFiRetryPhase::RESTARTING_ADAPTER: + // After restart, go back to explicit hidden if we went through it initially, otherwise scan + return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN + : WiFiRetryPhase::SCAN_CONNECTING; } - delay(10); - if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && - (this->num_retried_ > 3 || this->error_from_callback_)) { -#ifdef USE_WIFI_FAST_CONNECT - if (this->trying_loaded_ap_) { - this->trying_loaded_ap_ = false; - this->ap_index_ = 0; // Retry from the first configured AP - } else if (this->ap_index_ >= this->sta_.size() - 1) { - ESP_LOGW(TAG, "No more APs to try"); - this->ap_index_ = 0; - this->restart_adapter(); - } else { - // Try next AP - this->ap_index_++; - } - this->num_retried_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; -#else - if (this->num_retried_ > 5) { - // If retry failed for more than 5 times, let's restart STA - this->restart_adapter(); - } else { - // Try hidden networks after 3 failed retries - ESP_LOGD(TAG, "Retrying with hidden networks"); - this->retry_hidden_ = true; - this->num_retried_++; - } -#endif - } else { - this->num_retried_++; + // Should never reach here + return WiFiRetryPhase::SCAN_CONNECTING; +} + +/// Transition from current retry phase to a new phase with logging and phase-specific setup +/// This function handles the actual state change, including: +/// - Logging the phase transition +/// - Resetting the retry counter +/// - Performing phase-specific initialization (e.g., advancing AP index, starting scans) +/// +/// @param new_phase The phase we're transitioning TO +/// @return true if connection attempt should be skipped (scan started or no networks to try) +/// false if caller can proceed with connection attempt +bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { + WiFiRetryPhase old_phase = this->retry_phase_; + + // No-op if staying in same phase + if (old_phase == new_phase) { + return false; } - this->error_from_callback_ = false; - if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { - yield(); - this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - this->start_connecting(this->selected_ap_, true); + + ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)), + LOG_STR_ARG(retry_phase_to_log_string(new_phase))); + + this->retry_phase_ = new_phase; + this->num_retried_ = 0; // Reset retry counter on phase change + + // Phase-specific setup + switch (new_phase) { +#ifdef USE_WIFI_FAST_CONNECT + case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: + // Move to next configured AP - clear old scan data so new AP is tried with config only + this->selected_sta_index_++; + this->scan_result_.clear(); + break; +#endif + + case WiFiRetryPhase::EXPLICIT_HIDDEN: + // Starting explicit hidden phase - reset to first network + this->selected_sta_index_ = 0; + break; + + case WiFiRetryPhase::SCAN_CONNECTING: + // Transitioning to scan-based connection +#ifdef USE_WIFI_FAST_CONNECT + if (old_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) { + ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan"); + } +#endif + // Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan + if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN || + old_phase == WiFiRetryPhase::RETRY_HIDDEN || old_phase == WiFiRetryPhase::RESTARTING_ADAPTER) { + this->selected_sta_index_ = -1; // Will be set after scan completes + this->start_scanning(); + return true; // Started scan, wait for completion + } + // Already have scan results - selected_sta_index_ should already be synchronized + // (set in check_scanning_finished() when scan completed) + // No need to reset it here + break; + + case WiFiRetryPhase::RETRY_HIDDEN: + // Starting hidden mode - find first SSID that wasn't in scan results + if (old_phase == WiFiRetryPhase::SCAN_CONNECTING) { + // Keep scan results so we can skip SSIDs that were visible in the scan + // Don't clear scan_result_ - we need it to know which SSIDs are NOT hidden + + // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase + // In that case, skip networks marked hidden:true (already tried) + // Otherwise, include them (they haven't been tried yet) + this->selected_sta_index_ = this->find_next_hidden_sta_(-1); + + if (this->selected_sta_index_ == -1) { + ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode"); + } + } + break; + + case WiFiRetryPhase::RESTARTING_ADAPTER: + // Skip actual adapter restart if captive portal/improv is active + // This allows state machine to reset num_retried_ and trigger fresh scan + // without disrupting the captive portal/improv connection + if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { + this->restart_adapter(); + } + // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting + return true; + + default: + break; + } + + return false; // Did not start scan, can proceed with connection +} + +/// Clear BSSID priority tracking if all priorities are at minimum (saves memory) +/// At minimum priority, all BSSIDs are equally bad, so priority tracking is useless +/// Called after successful connection or after failed connection attempts +void WiFiComponent::clear_priorities_if_all_min_() { + if (this->sta_priorities_.empty()) { return; } - this->state_ = WIFI_COMPONENT_STATE_COOLDOWN; - this->action_started_ = millis(); + int8_t first_priority = this->sta_priorities_[0].priority; + + // Only clear if all priorities have been decremented to the minimum value + // At this point, all BSSIDs have been equally penalized and priority info is useless + if (first_priority != std::numeric_limits::min()) { + return; + } + + for (const auto &pri : this->sta_priorities_) { + if (pri.priority != first_priority) { + return; // Not all same, nothing to do + } + } + + // All priorities are at minimum - clear the vector to save memory and reset + ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)"); + this->sta_priorities_.clear(); + this->sta_priorities_.shrink_to_fit(); } -bool WiFiComponent::can_proceed() { - if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) { - return true; +/// Log failed connection attempt and decrease BSSID priority to avoid repeated failures +/// This function identifies which BSSID was attempted (from scan results or config), +/// decreases its priority by 1.0 to discourage future attempts, and logs the change. +/// +/// The priority degradation system ensures that failed BSSIDs are automatically sorted +/// lower in subsequent scans, naturally cycling through different APs without explicit +/// BSSID tracking within a scan cycle. +/// +/// Priority sources: +/// - SCAN_CONNECTING phase: Uses BSSID from scan_result_[0] (best match after sorting) +/// - Other phases: Uses BSSID from config if explicitly specified by user or fast_connect +/// +/// If no BSSID is available (SSID-only connection), priority adjustment is skipped. +/// +/// IMPORTANT: Priority is only decreased on the LAST attempt for a BSSID in SCAN_CONNECTING phase. +/// This prevents false positives from transient WiFi stack state issues after scanning. +/// Single failures don't necessarily mean the AP is bad - two genuine failures provide +/// higher confidence before degrading priority and skipping the BSSID in future scans. +void WiFiComponent::log_and_adjust_priority_for_failed_connect_() { + // Determine which BSSID we tried to connect to + optional failed_bssid; + + if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { + // Scan-based phase: always use best result (index 0) + failed_bssid = this->scan_result_[0].get_bssid(); + } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) { + // Config has specific BSSID (fast_connect or user-specified) + failed_bssid = *config->get_bssid(); } - return this->is_connected(); + + if (!failed_bssid.has_value()) { + return; // No BSSID to penalize + } + + // Get SSID for logging + std::string ssid; + if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) { + ssid = this->scan_result_[0].get_ssid(); + } else if (const WiFiAP *config = this->get_selected_sta_()) { + ssid = config->get_ssid(); + } + + // Only decrease priority on the last attempt for this phase + // This prevents false positives from transient WiFi stack issues + uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_); + bool is_last_attempt = (this->num_retried_ + 1 >= max_retries); + + // Decrease priority only on last attempt to avoid false positives from transient failures + int8_t old_priority = this->get_sta_priority(failed_bssid.value()); + int8_t new_priority = old_priority; + + if (is_last_attempt) { + // Decrease priority, but clamp to int8_t::min to prevent overflow + new_priority = + (old_priority > std::numeric_limits::min()) ? (old_priority - 1) : std::numeric_limits::min(); + this->set_sta_priority(failed_bssid.value(), new_priority); + } + + char bssid_s[18]; + format_mac_addr_upper(failed_bssid.value().data(), bssid_s); + ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s, + old_priority, new_priority); + + // After adjusting priority, check if all priorities are now at minimum + // If so, clear the vector to save memory and reset for fresh start + this->clear_priorities_if_all_min_(); } + +/// Handle target advancement or retry counter increment when staying in the same phase +/// This function is called when a connection attempt fails and determine_next_phase_() indicates +/// we should stay in the current phase. It decides whether to: +/// - Advance to the next target (AP in fast_connect, SSID in hidden mode) +/// - Or increment the retry counter to try the same target again +/// +/// Phase-specific behavior: +/// - FAST_CONNECT_CYCLING_APS: Always advance to next AP (no retries per AP) +/// - RETRY_HIDDEN: Advance to next SSID after exhausting retries on current SSID +/// - Other phases: Increment retry counter (will retry same target) +void WiFiComponent::advance_to_next_target_or_increment_retry_() { + WiFiRetryPhase current_phase = this->retry_phase_; + + // Check if we need to advance to next AP/SSID within the same phase +#ifdef USE_WIFI_FAST_CONNECT + if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) { + // Fast connect: always advance to next AP (no retries per AP) + this->selected_sta_index_++; + this->num_retried_ = 0; + ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); + return; + } +#endif + + if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) { + // Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network + // Stop when we reach a visible network (proceed to scanning) + size_t next_index = this->selected_sta_index_ + 1; + if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) { + this->selected_sta_index_ = static_cast(next_index); + this->num_retried_ = 0; + ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast(next_index)); + return; + } + // No more consecutive explicit hidden networks found - fall through to trigger phase change + } + + if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) { + // Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID + // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase + // In that case, skip networks marked hidden:true (already tried) + // Otherwise, include them (they haven't been tried yet) + int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_); + if (next_index != -1) { + // Found another potentially hidden SSID + this->selected_sta_index_ = next_index; + this->num_retried_ = 0; + return; + } + // No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change + // This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out + this->selected_sta_index_ = -1; + // Return early - phase change will happen on next wifi_loop() iteration + return; + } + + // Don't increment retry counter if we're in a scan phase with no valid targets + if (this->needs_scan_results_()) { + return; + } + + // Increment retry counter to try the same target again + this->num_retried_++; + ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1, + get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); +} + +void WiFiComponent::retry_connect() { + this->log_and_adjust_priority_for_failed_connect_(); + + // Determine next retry phase based on current state + WiFiRetryPhase current_phase = this->retry_phase_; + WiFiRetryPhase next_phase = this->determine_next_phase_(); + + // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally) + if (this->transition_to_phase_(next_phase)) { + return; // Scan started or adapter restarted (which sets its own state) + } + + if (next_phase == current_phase) { + this->advance_to_next_target_or_increment_retry_(); + } + + this->error_from_callback_ = false; + + yield(); + // Check if we have a valid target before building params + // After exhausting all networks in a phase, selected_sta_index_ may be -1 + // In that case, skip connection and let next wifi_loop() handle phase transition + if (this->selected_sta_index_ >= 0) { + WiFiAP params = this->build_params_for_current_phase_(); + this->start_connecting(params); + } +} + void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } bool WiFiComponent::is_connected() { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && @@ -852,16 +1545,29 @@ bool WiFiComponent::is_esp32_improv_active_() { } #ifdef USE_WIFI_FAST_CONNECT -bool WiFiComponent::load_fast_connect_settings_() { +bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { + // Validate saved AP index + if (fast_connect_save.ap_index < 0 || static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { + ESP_LOGW(TAG, "AP index out of bounds"); + return false; + } + + // Set selected index for future operations (save, retry, etc) + this->selected_sta_index_ = fast_connect_save.ap_index; + + // Copy entire config, then override with fast connect data + params = this->sta_[fast_connect_save.ap_index]; + + // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config) bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); - this->ap_index_ = fast_connect_save.ap_index; - this->selected_ap_ = this->sta_[this->ap_index_]; - this->selected_ap_.set_bssid(bssid); - this->selected_ap_.set_channel(fast_connect_save.channel); + params.set_bssid(bssid); + params.set_channel(fast_connect_save.channel); + // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true) + params.set_hidden(false); ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; @@ -873,18 +1579,25 @@ bool WiFiComponent::load_fast_connect_settings_() { void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); + // selected_sta_index_ is always valid here (called only after successful connection) + // Fallback to 0 is defensive programming for robustness + int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; - if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) { - SavedWifiFastConnectSettings fast_connect_save{}; - - memcpy(fast_connect_save.bssid, bssid.data(), 6); - fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->ap_index_; - - this->fast_connect_pref_.save(&fast_connect_save); - - ESP_LOGD(TAG, "Saved fast_connect settings"); + // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear) + SavedWifiFastConnectSettings previous_save{}; + if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 && + previous_save.channel == channel && previous_save.ap_index == ap_index) { + return; // No change, nothing to save } + + SavedWifiFastConnectSettings fast_connect_save{}; + memcpy(fast_connect_save.bssid, bssid.data(), 6); + fast_connect_save.channel = channel; + fast_connect_save.ap_index = ap_index; + + this->fast_connect_pref_.save(&fast_connect_save); + + ESP_LOGD(TAG, "Saved fast_connect settings"); } #endif @@ -896,7 +1609,9 @@ void WiFiAP::set_password(const std::string &password) { this->password_ = passw void WiFiAP::set_eap(optional eap_auth) { this->eap_ = std::move(eap_auth); } #endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } +#ifdef USE_WIFI_MANUAL_IP void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } +#endif void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } @@ -905,15 +1620,17 @@ const std::string &WiFiAP::get_password() const { return this->password_; } const optional &WiFiAP::get_eap() const { return this->eap_; } #endif const optional &WiFiAP::get_channel() const { return this->channel_; } +#ifdef USE_WIFI_MANUAL_IP const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } +#endif bool WiFiAP::get_hidden() const { return this->hidden_; } WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden) : bssid_(bssid), - ssid_(std::move(ssid)), channel_(channel), rssi_(rssi), + ssid_(std::move(ssid)), with_auth_(with_auth), is_hidden_(is_hidden) {} bool WiFiScanResult::matches(const WiFiAP &config) const { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 42f78dbfac8..2fd7fa6cd43 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -52,6 +52,9 @@ extern "C" { namespace esphome { namespace wifi { +/// Sentinel value for RSSI when WiFi is not connected +static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; + struct SavedWifiSettings { char ssid[33]; char password[65]; @@ -74,12 +77,6 @@ enum WiFiComponentState : uint8_t { WIFI_COMPONENT_STATE_STA_SCANNING, /** WiFi is in STA(+AP) mode and currently connecting to an AP. */ WIFI_COMPONENT_STATE_STA_CONNECTING, - /** WiFi is in STA(+AP) mode and currently connecting to an AP a second time. - * - * This is required because for some reason ESPs don't like to connect to WiFi APs directly after - * a scan. - * */ - WIFI_COMPONENT_STATE_STA_CONNECTING_2, /** WiFi is in STA(+AP) mode and successfully connected. */ WIFI_COMPONENT_STATE_STA_CONNECTED, /** WiFi is in AP-only mode and internal AP is already enabled. */ @@ -94,6 +91,24 @@ enum class WiFiSTAConnectStatus : int { ERROR_CONNECT_FAILED, }; +/// Tracks the current retry strategy/phase for WiFi connection attempts +enum class WiFiRetryPhase : uint8_t { + /// Initial connection attempt (varies based on fast_connect setting) + INITIAL_CONNECT, +#ifdef USE_WIFI_FAST_CONNECT + /// Fast connect mode: cycling through configured APs (config-only, no scan) + FAST_CONNECT_CYCLING_APS, +#endif + /// Explicitly hidden networks (user marked as hidden, try before scanning) + EXPLICIT_HIDDEN, + /// Scan-based: connecting to best AP from scan results + SCAN_CONNECTING, + /// Retry networks not found in scan (might be hidden) + RETRY_HIDDEN, + /// Restarting WiFi adapter to clear stuck state + RESTARTING_ADAPTER, +}; + /// Struct for setting static IPs in WiFiComponent. struct ManualIP { network::IPAddress static_ip; @@ -139,8 +154,10 @@ class WiFiAP { void set_eap(optional eap_auth); #endif // USE_WIFI_WPA2_EAP void set_channel(optional channel); - void set_priority(float priority) { priority_ = priority; } + void set_priority(int8_t priority) { priority_ = priority; } +#ifdef USE_WIFI_MANUAL_IP void set_manual_ip(optional manual_ip); +#endif void set_hidden(bool hidden); const std::string &get_ssid() const; const optional &get_bssid() const; @@ -149,8 +166,10 @@ class WiFiAP { const optional &get_eap() const; #endif // USE_WIFI_WPA2_EAP const optional &get_channel() const; - float get_priority() const { return priority_; } + int8_t get_priority() const { return priority_; } +#ifdef USE_WIFI_MANUAL_IP const optional &get_manual_ip() const; +#endif bool get_hidden() const; protected: @@ -160,9 +179,11 @@ class WiFiAP { #ifdef USE_WIFI_WPA2_EAP optional eap_; #endif // USE_WIFI_WPA2_EAP +#ifdef USE_WIFI_MANUAL_IP optional manual_ip_; - float priority_{0}; +#endif optional channel_; + int8_t priority_{0}; bool hidden_{false}; }; @@ -180,17 +201,17 @@ class WiFiScanResult { int8_t get_rssi() const; bool get_with_auth() const; bool get_is_hidden() const; - float get_priority() const { return priority_; } - void set_priority(float priority) { priority_ = priority; } + int8_t get_priority() const { return priority_; } + void set_priority(int8_t priority) { priority_ = priority; } bool operator==(const WiFiScanResult &rhs) const; protected: bssid_t bssid_; - std::string ssid_; - float priority_{0.0f}; uint8_t channel_; int8_t rssi_; + std::string ssid_; + int8_t priority_{0}; bool matches_{false}; bool with_auth_; bool is_hidden_; @@ -198,7 +219,7 @@ class WiFiScanResult { struct WiFiSTAPriority { bssid_t bssid; - float priority; + int8_t priority; }; enum WiFiPowerSaveMode : uint8_t { @@ -207,6 +228,12 @@ enum WiFiPowerSaveMode : uint8_t { WIFI_POWER_SAVE_HIGH, }; +enum WifiMinAuthMode : uint8_t { + WIFI_MIN_AUTH_MODE_WPA = 0, + WIFI_MIN_AUTH_MODE_WPA2, + WIFI_MIN_AUTH_MODE_WPA3, +}; + #ifdef USE_ESP32 struct IDFWiFiEvent; #endif @@ -218,10 +245,14 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); - WiFiAP get_sta() { return this->selected_ap_; } + // Returns a copy of the currently selected AP configuration + WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); - void clear_sta(); + void clear_sta() { + this->sta_.clear(); + this->selected_sta_index_ = -1; + } #ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. @@ -233,6 +264,7 @@ class WiFiComponent : public Component { */ void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } + void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } #endif // USE_WIFI_AP void enable(); @@ -240,20 +272,20 @@ class WiFiComponent : public Component { bool is_disabled(); void start_scanning(); void check_scanning_finished(); - void start_connecting(const WiFiAP &ap, bool two); - void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } + void start_connecting(const WiFiAP &ap); + // Backward compatibility overload - ignores 'two' parameter + void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); } void check_connecting_finished(); void retry_connect(); - bool can_proceed() override; - void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected(); void set_power_save_mode(WiFiPowerSaveMode power_save); + void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } void set_passive_scan(bool passive); @@ -283,8 +315,8 @@ class WiFiComponent : public Component { network::IPAddress get_dns_address(int num); network::IPAddresses get_ip_addresses(); - const std::string &get_use_address() const; - void set_use_address(const std::string &use_address); + const char *get_use_address() const; + void set_use_address(const char *use_address); const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } @@ -297,14 +329,14 @@ class WiFiComponent : public Component { } return false; } - float get_sta_priority(const bssid_t bssid) { + int8_t get_sta_priority(const bssid_t bssid) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) return it.priority; } - return 0.0f; + return 0; } - void set_sta_priority(const bssid_t bssid, float priority) { + void set_sta_priority(const bssid_t bssid, int8_t priority) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) { it.priority = priority; @@ -337,13 +369,67 @@ class WiFiComponent : public Component { #endif // USE_WIFI_AP void print_connect_params_(); + WiFiAP build_params_for_current_phase_(); + + /// Determine next retry phase based on current state and failure conditions + WiFiRetryPhase determine_next_phase_(); + /// Transition to a new retry phase with logging + /// Returns true if a scan was started (caller should wait), false otherwise + bool transition_to_phase_(WiFiRetryPhase new_phase); + /// Check if we need valid scan results for the current phase but don't have any + /// Returns true if the phase requires scan results but they're missing or don't match + bool needs_scan_results_() const; + /// Check if we went through EXPLICIT_HIDDEN phase (first network is marked hidden) + /// Used in RETRY_HIDDEN to determine whether to skip explicitly hidden networks + bool went_through_explicit_hidden_phase_() const; + /// Find the index of the first non-hidden network + /// Returns where EXPLICIT_HIDDEN phase would have stopped, or -1 if all networks are hidden + int8_t find_first_non_hidden_index_() const; + /// Check if an SSID was seen in the most recent scan results + /// Used to skip hidden mode for SSIDs we know are visible + bool ssid_was_seen_in_scan_(const std::string &ssid) const; + /// Find next SSID that wasn't in scan results (might be hidden) + /// Returns index of next potentially hidden SSID, or -1 if none found + /// @param start_index Start searching from index after this (-1 to start from beginning) + int8_t find_next_hidden_sta_(int8_t start_index); + /// Log failed connection and decrease BSSID priority to avoid repeated attempts + void log_and_adjust_priority_for_failed_connect_(); + /// Clear BSSID priority tracking if all priorities are at minimum (saves memory) + void clear_priorities_if_all_min_(); + /// Advance to next target (AP/SSID) within current phase, or increment retry counter + /// Called when staying in the same phase after a failed connection attempt + void advance_to_next_target_or_increment_retry_(); + /// Start initial connection - either scan or connect directly to hidden networks + void start_initial_connection_(); + const WiFiAP *get_selected_sta_() const { + if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { + return &this->sta_[this->selected_sta_index_]; + } + return nullptr; + } + + void reset_selected_ap_to_first_if_invalid_() { + if (this->selected_sta_index_ < 0 || static_cast(this->selected_sta_index_) >= this->sta_.size()) { + this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; + } + } + + bool all_networks_hidden_() const { + if (this->sta_.empty()) + return false; + for (const auto &ap : this->sta_) { + if (!ap.get_hidden()) + return false; + } + return true; + } void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); - bool wifi_sta_ip_config_(optional manual_ip); + bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); @@ -351,7 +437,7 @@ class WiFiComponent : public Component { bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP - bool wifi_ap_ip_config_(optional manual_ip); + bool wifi_ap_ip_config_(const optional &manual_ip); bool wifi_start_ap_(const WiFiAP &ap); #endif // USE_WIFI_AP @@ -365,7 +451,7 @@ class WiFiComponent : public Component { bool is_esp32_improv_active_(); #ifdef USE_WIFI_FAST_CONNECT - bool load_fast_connect_settings_(); + bool load_fast_connect_settings_(WiFiAP ¶ms); void save_fast_connect_settings_(); #endif @@ -393,12 +479,12 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif - std::string use_address_; FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; - WiFiAP selected_ap_; +#ifdef USE_WIFI_AP WiFiAP ap_; +#endif optional output_power_; ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT @@ -409,24 +495,26 @@ class WiFiComponent : public Component { uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; +#ifdef USE_WIFI_AP uint32_t ap_timeout_{}; +#endif // Group all 8-bit values together WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; + WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; + WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; uint8_t num_retried_{0}; -#ifdef USE_WIFI_FAST_CONNECT - uint8_t ap_index_{0}; -#endif + // Index into sta_ array for the currently selected AP configuration (-1 = none selected) + // Used to access password, manual_ip, priority, EAP settings, and hidden flag + // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) + int8_t selected_sta_index_{-1}; + #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ // Group all boolean values together -#ifdef USE_WIFI_FAST_CONNECT - bool trying_loaded_ap_{false}; -#endif - bool retry_hidden_{false}; bool has_ap_{false}; bool handled_connected_state_{false}; bool error_from_callback_{false}; @@ -445,28 +533,33 @@ class WiFiComponent : public Component { // Pointers at the end (naturally aligned) Trigger<> *connect_trigger_{new Trigger<>()}; Trigger<> *disconnect_trigger_{new Trigger<>()}; + + private: + // Stores a pointer to a string literal (static storage duration). + // ONLY set from Python-generated code with string literals - never dynamic strings. + const char *use_address_{""}; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) template class WiFiConnectedCondition : public Condition { public: - bool check(Ts... x) override { return global_wifi_component->is_connected(); } + bool check(const Ts &...x) override { return global_wifi_component->is_connected(); } }; template class WiFiEnabledCondition : public Condition { public: - bool check(Ts... x) override { return !global_wifi_component->is_disabled(); } + bool check(const Ts &...x) override { return !global_wifi_component->is_disabled(); } }; template class WiFiEnableAction : public Action { public: - void play(Ts... x) override { global_wifi_component->enable(); } + void play(const Ts &...x) override { global_wifi_component->enable(); } }; template class WiFiDisableAction : public Action { public: - void play(Ts... x) override { global_wifi_component->disable(); } + void play(const Ts &...x) override { global_wifi_component->disable(); } }; template class WiFiConfigureAction : public Action, public Component { @@ -476,7 +569,7 @@ template class WiFiConfigureAction : public Action, publi TEMPLATABLE_VALUE(bool, save) TEMPLATABLE_VALUE(uint32_t, connection_timeout) - void play(Ts... x) override { + void play(const Ts &...x) override { auto ssid = this->ssid_.value(x...); auto password = this->password_.value(x...); // Avoid multiple calls diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 4e17c42f413..a543628e278 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -117,7 +117,7 @@ void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t }; #endif -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -258,8 +258,17 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_password().empty()) { conf.threshold.authmode = AUTH_OPEN; } else { - // Only allow auth modes with at least WPA - conf.threshold.authmode = AUTH_WPA_PSK; + // Set threshold based on configured minimum auth mode + // Note: ESP8266 doesn't support WPA3 + switch (this->min_auth_mode_) { + case WIFI_MIN_AUTH_MODE_WPA: + conf.threshold.authmode = AUTH_WPA_PSK; + break; + case WIFI_MIN_AUTH_MODE_WPA2: + case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266 + conf.threshold.authmode = AUTH_WPA2_PSK; + break; + } } conf.threshold.rssi = -127; #endif @@ -273,9 +282,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP @@ -510,8 +525,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); s_sta_connect_not_found = true; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason))); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + LOG_STR_ARG(get_disconnect_reason_str(it.reason))); s_sta_connect_error = true; } s_sta_connected = false; @@ -715,7 +732,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; @@ -823,10 +840,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif return true; } @@ -848,7 +872,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; } diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 08ecba35987..4aac03885ae 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -308,7 +308,18 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (ap.get_password().empty()) { conf.sta.threshold.authmode = WIFI_AUTH_OPEN; } else { - conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; + // Set threshold based on configured minimum auth mode + switch (this->min_auth_mode_) { + case WIFI_MIN_AUTH_MODE_WPA: + conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK; + break; + case WIFI_MIN_AUTH_MODE_WPA2: + conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; + break; + case WIFI_MIN_AUTH_MODE_WPA3: + conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK; + break; + } } #ifdef USE_WIFI_WPA2_EAP @@ -347,8 +358,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // The minimum rssi to accept in the fast scan mode conf.sta.threshold.rssi = -127; - conf.sta.threshold.authmode = WIFI_AUTH_OPEN; - wifi_config_t current_conf; esp_err_t err; err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf); @@ -371,9 +380,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif // setup enterprise authentication if required #ifdef USE_WIFI_WPA2_EAP @@ -472,7 +487,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { return true; } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -731,8 +746,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf); return; } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); s_sta_connect_error = true; } s_sta_connected = false; @@ -869,7 +886,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esp_err_t err; // enable AP @@ -985,10 +1002,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return false; } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:"); + return false; + } +#endif return true; } @@ -1007,7 +1031,8 @@ bssid_t WiFiComponent::wifi_bssid() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return bssid; } std::copy(info.bssid, info.bssid + 6, bssid.begin()); @@ -1017,7 +1042,8 @@ std::string WiFiComponent::wifi_ssid() { wifi_ap_record_t info{}; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); return ""; } auto *ssid_s = reinterpret_cast(info.ssid); @@ -1028,8 +1054,9 @@ int8_t WiFiComponent::wifi_rssi() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); if (err != ESP_OK) { - ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); - return 0; + // Very verbose only: this is expected during dump_config() before connection is established (PR #9823) + ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err)); + return WIFI_RSSI_DISCONNECTED; } return info.rssi; } diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 45e2fba82a7..98cbfddb1da 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -68,7 +68,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() { return true; } bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -112,9 +112,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { WiFi.disconnect(); } +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) { return false; } +#else + if (!this->wifi_sta_ip_config_({})) { + return false; + } +#endif this->wifi_apply_hostname_(); @@ -293,8 +299,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ if (it.reason == WIFI_REASON_NO_AP_FOUND) { ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf); } else { - ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, - format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason)); + char bssid_s[18]; + format_mac_addr_upper(it.bssid, bssid_s); + ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s, + get_disconnect_reason_str(it.reason)); } uint8_t reason = it.reason; @@ -428,7 +436,7 @@ void WiFiComponent::wifi_scan_done_callback_() { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { // enable AP if (!this->wifi_mode_({}, true)) return false; @@ -445,10 +453,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif yield(); @@ -471,7 +486,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index bf15892cd5e..91766e8ab55 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -55,8 +55,13 @@ bool WiFiComponent::wifi_apply_power_save_() { bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) return false; +#else + if (!this->wifi_sta_ip_config_({})) + return false; +#endif auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str()); if (ret != WL_CONNECTED) @@ -67,7 +72,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); } -bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_sta_ip_config_(const optional &manual_ip) { if (!manual_ip.has_value()) { return true; } @@ -141,7 +146,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { } #ifdef USE_WIFI_AP -bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { +bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { esphome::network::IPAddress ip_address, gateway, subnet, dns; if (manual_ip.has_value()) { ip_address = manual_ip->static_ip; @@ -161,10 +166,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { if (!this->wifi_mode_({}, true)) return false; +#ifdef USE_WIFI_MANUAL_IP if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) { ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); return false; } +#else + if (!this->wifi_ap_ip_config_({})) { + ESP_LOGV(TAG, "wifi_ap_ip_config_ failed"); + return false; + } +#endif WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1)); @@ -188,7 +200,7 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } -int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); } +int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() { diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 2cb96123a0f..04889d6bb3c 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -10,6 +10,8 @@ namespace esphome { namespace wifi_info { +static constexpr size_t MAX_STATE_LENGTH = 255; + class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { public: void update() override { @@ -71,11 +73,14 @@ class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSen scan_results += "dB\n"; } + // There's a limit of 255 characters per state. + // Longer states just don't get sent so we truncate it. + if (scan_results.length() > MAX_STATE_LENGTH) { + scan_results.resize(MAX_STATE_LENGTH); + } if (this->last_scan_results_ != scan_results) { this->last_scan_results_ = scan_results; - // There's a limit of 255 characters per state. - // Longer states just don't get sent so we truncate it. - this->publish_state(scan_results.substr(0, 255)); + this->publish_state(scan_results); } } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } diff --git a/esphome/components/wireguard/binary_sensor.py b/esphome/components/wireguard/binary_sensor.py index 02c4862e8d9..2ba59d4c39d 100644 --- a/esphome/components/wireguard/binary_sensor.py +++ b/esphome/components/wireguard/binary_sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import binary_sensor +from esphome.components.const import CONF_ENABLED import esphome.config_validation as cv from esphome.const import ( CONF_STATUS, @@ -9,8 +10,6 @@ from esphome.const import ( from . import CONF_WIREGUARD_ID, Wireguard -CONF_ENABLED = "enabled" - DEPENDENCIES = ["wireguard"] CONFIG_SCHEMA = { diff --git a/esphome/components/wireguard/wireguard.h b/esphome/components/wireguard/wireguard.h index 5db9a48c909..f8f79b835d8 100644 --- a/esphome/components/wireguard/wireguard.h +++ b/esphome/components/wireguard/wireguard.h @@ -148,25 +148,25 @@ std::string mask_key(const std::string &key); /// Condition to check if remote peer is online. template class WireguardPeerOnlineCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_peer_up(); } + bool check(const Ts &...x) override { return this->parent_->is_peer_up(); } }; /// Condition to check if Wireguard component is enabled. template class WireguardEnabledCondition : public Condition, public Parented { public: - bool check(Ts... x) override { return this->parent_->is_enabled(); } + bool check(const Ts &...x) override { return this->parent_->is_enabled(); } }; /// Action to enable Wireguard component. template class WireguardEnableAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->enable(); } + void play(const Ts &...x) override { this->parent_->enable(); } }; /// Action to disable Wireguard component. template class WireguardDisableAction : public Action, public Parented { public: - void play(Ts... x) override { this->parent_->disable(); } + void play(const Ts &...x) override { this->parent_->disable(); } }; } // namespace wireguard diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index a2fb12a5e22..0381fbcba9d 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -1,4 +1,5 @@ from pathlib import Path +import textwrap from typing import TypedDict import esphome.codegen as cg @@ -90,7 +91,7 @@ def zephyr_add_prj_conf( def zephyr_add_overlay(content): - zephyr_data()[KEY_OVERLAY] += content + zephyr_data()[KEY_OVERLAY] += textwrap.dedent(content) def add_extra_build_file(filename: str, path: Path) -> bool: diff --git a/esphome/components/zephyr/core.cpp b/esphome/components/zephyr/core.cpp index 365b6b8ed2d..d5427a0ebfc 100644 --- a/esphome/components/zephyr/core.cpp +++ b/esphome/components/zephyr/core.cpp @@ -6,6 +6,7 @@ #include #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/defines.h" namespace esphome { @@ -25,7 +26,14 @@ void arch_init() { wdt_config.window.max = 2000; wdt_channel_id = wdt_install_timeout(WDT, &wdt_config); if (wdt_channel_id >= 0) { - wdt_setup(WDT, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP); + uint8_t options = 0; +#ifdef USE_DEBUG + options |= WDT_OPT_PAUSE_HALTED_BY_DBG; +#endif +#ifdef USE_DEEP_SLEEP + options |= WDT_OPT_PAUSE_IN_SLEEP; +#endif + wdt_setup(WDT, options); } } } diff --git a/esphome/const.py b/esphome/const.py index 3bbc6b8b3fd..a25114d80e8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.11.0-dev" +__version__ = "2025.12.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( @@ -838,7 +838,6 @@ CONF_RMT_CHANNEL = "rmt_channel" CONF_RMT_SYMBOLS = "rmt_symbols" CONF_ROTATION = "rotation" CONF_ROW = "row" -CONF_ROWS = "rows" CONF_RS_PIN = "rs_pin" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_WIRES = "rtd_wires" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index e1f1f688f0c..e0aa5bd4d53 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -48,6 +48,9 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +# Key for tracking controller count in CORE.data for ControllerRegistry StaticVector sizing +KEY_CONTROLLER_REGISTRY_COUNT = "controller_registry_count" + class EsphomeError(Exception): """General ESPHome exception occurred.""" @@ -919,6 +922,11 @@ class EsphomeCore: """ self.platform_counts[platform_name] += 1 + def register_controller(self) -> None: + """Track registration of a Controller for ControllerRegistry StaticVector sizing.""" + controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0) + self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1 + @property def cpp_main_section(self): from esphome.cpp_generator import statement diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 61cfcc75853..75814ae2535 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -122,6 +122,11 @@ void Application::setup() { // Clear setup priority overrides to free memory clear_setup_priority_overrides(); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Set up wake socket for waking main loop from tasks + this->setup_wake_loop_threadsafe_(); +#endif + this->schedule_dump_config(); } void Application::loop() { @@ -472,6 +477,11 @@ void Application::enable_pending_loops_() { } void Application::before_loop_tasks_(uint32_t loop_start_time) { +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + // Drain wake notifications first to clear socket for next wake + this->drain_wake_notifications_(); +#endif + // Process scheduled tasks this->scheduler.call(loop_start_time); @@ -625,4 +635,73 @@ void Application::yield_with_select_(uint32_t delay_ms) { Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) +void Application::setup_wake_loop_threadsafe_() { + // Create UDP socket for wake notifications + this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (this->wake_socket_fd_ < 0) { + ESP_LOGW(TAG, "Wake socket create failed: %d", errno); + return; + } + + // 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_port = 0; // Auto-assign port + + if (lwip_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_); + this->wake_socket_fd_ = -1; + return; + } + + // Get the assigned address and connect to it + // 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) { + ESP_LOGW(TAG, "Wake socket address failed: %d", errno); + lwip_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) { + ESP_LOGW(TAG, "Wake socket connect failed: %d", errno); + lwip_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); + + // 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_); + 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 // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 29a734f000b..dae44d89027 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -21,7 +21,10 @@ #ifdef USE_SOCKET_SELECT_SUPPORT #include +#ifdef USE_WAKE_LOOP_THREADSAFE +#include #endif +#endif // USE_SOCKET_SELECT_SUPPORT #ifdef USE_BINARY_SENSOR #include "esphome/components/binary_sensor/binary_sensor.h" @@ -429,6 +432,13 @@ class Application { /// Check if there's data available on a socket without blocking /// This function is thread-safe for reading, but should be called after select() has run bool is_socket_ready(int fd) const; + +#ifdef USE_WAKE_LOOP_THREADSAFE + /// Wake the main event loop from a FreeRTOS task + /// Thread-safe, can be called from task context to immediately wake select() + /// IMPORTANT: NOT safe to call from ISR context (socket operations not ISR-safe) + void wake_loop_threadsafe(); +#endif #endif protected: @@ -454,6 +464,11 @@ class Application { /// Perform a delay while also monitoring socket file descriptors for readiness void yield_with_select_(uint32_t delay_ms); +#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + 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 + // === Member variables ordered by size to minimize padding === // Pointer-sized members first @@ -481,6 +496,9 @@ class Application { FixedVector looping_components_{}; #ifdef USE_SOCKET_SELECT_SUPPORT std::vector socket_fds_; // Vector of all monitored socket file descriptors +#ifdef USE_WAKE_LOOP_THREADSAFE + int wake_socket_fd_{-1}; // Shared wake notification socket for waking main loop from tasks +#endif #endif // std::string members (typically 24-32 bytes each) @@ -597,4 +615,28 @@ 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) +// Inline implementations for hot-path functions +// drain_wake_notifications_() is called on every loop iteration + +// Small buffer for draining wake notification bytes (1 byte sent per wake) +// Size allows draining multiple notifications per recvfrom() without wasting stack +static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16; + +inline void Application::drain_wake_notifications_() { + // Called from main loop to drain any pending wake notifications + // Must check is_socket_ready() to avoid blocking on empty socket + if (this->wake_socket_fd_ >= 0 && this->is_socket_ready(this->wake_socket_fd_)) { + char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE]; + // Drain all pending notifications with non-blocking reads + // 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) { + // Just draining, no action needed - wake has already occurred + } + } +} +#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) + } // namespace esphome diff --git a/esphome/core/automation.h b/esphome/core/automation.h index c22b3ca0e35..97c9e9c3cc2 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -11,10 +11,27 @@ namespace esphome { +// C++20 std::index_sequence is now used for tuple unpacking +// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility // https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971 -template struct seq {}; // NOLINT -template struct gens : gens {}; // NOLINT -template struct gens<0, S...> { using type = seq; }; // NOLINT +// Remove before 2026.6.0 +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +// NOLINTNEXTLINE(readability-identifier-naming) +template struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {}; +// NOLINTNEXTLINE(readability-identifier-naming) +template +struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens + : gens {}; +// NOLINTNEXTLINE(readability-identifier-naming) +template struct gens<0, S...> { using type = seq; }; + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif #define TEMPLATABLE_VALUE_(type, name) \ protected: \ @@ -148,15 +165,15 @@ template class TemplatableValue { template class Condition { public: /// Check whether this condition passes. This condition check must be instant, and not cause any delays. - virtual bool check(Ts... x) = 0; + virtual bool check(const Ts &...x) = 0; /// Call check with a tuple of values as parameter. bool check_tuple(const std::tuple &tuple) { - return this->check_tuple_(tuple, typename gens::type()); + return this->check_tuple_(tuple, std::make_index_sequence{}); } protected: - template bool check_tuple_(const std::tuple &tuple, seq /*unused*/) { + template bool check_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { return this->check(std::get(tuple)...); } }; @@ -166,7 +183,7 @@ template class Automation; template class Trigger { public: /// Inform the parent automation that the event has triggered. - void trigger(Ts... x) { + void trigger(const Ts &...x) { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); @@ -194,7 +211,7 @@ template class ActionList; template class Action { public: - virtual void play_complex(Ts... x) { + virtual void play_complex(const Ts &...x) { this->num_running_++; this->play(x...); this->play_next_(x...); @@ -222,8 +239,8 @@ template class Action { friend ActionList; template friend class ContinuationAction; - virtual void play(Ts... x) = 0; - void play_next_(Ts... x) { + virtual void play(const Ts &...x) = 0; + void play_next_(const Ts &...x) { if (this->num_running_ > 0) { this->num_running_--; if (this->next_ != nullptr) { @@ -231,11 +248,11 @@ template class Action { } } } - template void play_next_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_next_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play_next_(std::get(tuple)...); } void play_next_tuple_(const std::tuple &tuple) { - this->play_next_tuple_(tuple, typename gens::type()); + this->play_next_tuple_(tuple, std::make_index_sequence{}); } virtual void stop() {} @@ -273,11 +290,13 @@ template class ActionList { this->add_action(action); } } - void play(Ts... x) { + void play(const Ts &...x) { if (this->actions_begin_ != nullptr) this->actions_begin_->play_complex(x...); } - void play_tuple(const std::tuple &tuple) { this->play_tuple_(tuple, typename gens::type()); } + 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(); @@ -298,7 +317,7 @@ template class ActionList { } protected: - template void play_tuple_(const std::tuple &tuple, seq /*unused*/) { + template void play_tuple_(const std::tuple &tuple, std::index_sequence /*unused*/) { this->play(std::get(tuple)...); } @@ -315,7 +334,7 @@ template class Automation { void stop() { this->actions_.stop(); } - void trigger(Ts... x) { this->actions_.play(x...); } + void trigger(const Ts &...x) { this->actions_.play(x...); } bool is_running() { return this->actions_.is_running(); } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 083bb3ae311..c2519da839a 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -17,7 +17,7 @@ namespace esphome { template class AndCondition : public Condition { public: explicit AndCondition(std::initializer_list *> conditions) : conditions_(conditions) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (!condition->check(x...)) return false; @@ -33,7 +33,7 @@ template class AndCondition : public Condition { template class OrCondition : public Condition { public: explicit OrCondition(std::initializer_list *> conditions) : conditions_(conditions) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (condition->check(x...)) return true; @@ -49,7 +49,7 @@ template class OrCondition : public Condition { template class NotCondition : public Condition { public: explicit NotCondition(Condition *condition) : condition_(condition) {} - bool check(Ts... x) override { return !this->condition_->check(x...); } + bool check(const Ts &...x) override { return !this->condition_->check(x...); } protected: Condition *condition_; @@ -58,7 +58,7 @@ template class NotCondition : public Condition { template class XorCondition : public Condition { public: explicit XorCondition(std::initializer_list *> conditions) : conditions_(conditions) {} - bool check(Ts... x) override { + bool check(const Ts &...x) override { size_t result = 0; for (auto *condition : this->conditions_) { result += condition->check(x...); @@ -74,7 +74,7 @@ template class XorCondition : public Condition { template class LambdaCondition : public Condition { public: explicit LambdaCondition(std::function &&f) : f_(std::move(f)) {} - bool check(Ts... x) override { return this->f_(x...); } + bool check(const Ts &...x) override { return this->f_(x...); } protected: std::function f_; @@ -86,7 +86,7 @@ template class LambdaCondition : public Condition { template class StatelessLambdaCondition : public Condition { public: explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {} - bool check(Ts... x) override { return this->f_(x...); } + bool check(const Ts &...x) override { return this->f_(x...); } protected: bool (*f_)(Ts...); @@ -98,22 +98,28 @@ template class ForCondition : public Condition, public Co TEMPLATABLE_VALUE(uint32_t, time); - void loop() override { this->check_internal(); } - float get_setup_priority() const override { return setup_priority::DATA; } - bool check_internal() { - bool cond = this->condition_->check(); - if (!cond) - this->last_inactive_ = App.get_loop_component_start_time(); - return cond; + void loop() override { + // Safe to use cached time - only called from Application::loop() + this->check_internal_(App.get_loop_component_start_time()); } - bool check(Ts... x) override { - if (!this->check_internal()) + float get_setup_priority() const override { return setup_priority::DATA; } + + bool check(const Ts &...x) override { + auto now = millis(); + if (!this->check_internal_(now)) return false; - return millis() - this->last_inactive_ >= this->time_.value(x...); + return now - this->last_inactive_ >= this->time_.value(x...); } protected: + bool check_internal_(uint32_t now) { + bool cond = this->condition_->check(); + if (!cond) + this->last_inactive_ = now; + return cond; + } + Condition<> *condition_; uint32_t last_inactive_{0}; }; @@ -171,8 +177,7 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) - void play_complex(Ts... x) override { - auto f = std::bind(&DelayAction::play_next_, this, x...); + void play_complex(const Ts &...x) override { this->num_running_++; // If num_running_ > 1, we have multiple instances running in parallel @@ -181,13 +186,26 @@ template class DelayAction : public Action, public Compon // WARNING: This can accumulate delays if scripts are triggered faster than they complete! // Users should set max_runs on parallel scripts to limit concurrent executions. // Issue #10264: This is a workaround for parallel script delays interfering with each other. - App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, - /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), - /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + + // Optimization: For no-argument delays (most common case), use direct lambda + // instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution) + if constexpr (sizeof...(Ts) == 0) { + App.scheduler.set_timer_common_( + this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(), [this]() { this->play_next_(); }, + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } else { + // For delays with arguments, use std::bind to preserve argument values + // Arguments must be copied because original references may be invalid after delay + auto f = std::bind(&DelayAction::play_next_, this, x...); + App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); + } } float get_setup_priority() const override { return setup_priority::HARDWARE; } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->cancel_timeout("delay"); } @@ -197,7 +215,7 @@ template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} - void play(Ts... x) override { this->f_(x...); } + void play(const Ts &...x) override { this->f_(x...); } protected: std::function f_; @@ -210,7 +228,7 @@ template class StatelessLambdaAction : public Action { public: explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {} - void play(Ts... x) override { this->f_(x...); } + void play(const Ts &...x) override { this->f_(x...); } protected: void (*f_)(Ts...); @@ -223,7 +241,7 @@ template class ContinuationAction : public Action { public: explicit ContinuationAction(Action *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->play_next_(x...); } + void play(const Ts &...x) override { this->parent_->play_next_(x...); } protected: Action *parent_; @@ -238,7 +256,7 @@ template class WhileLoopContinuation : public Action { public: explicit WhileLoopContinuation(WhileAction *parent) : parent_(parent) {} - void play(Ts... x) override; + void play(const Ts &...x) override; protected: WhileAction *parent_; @@ -258,7 +276,7 @@ template class IfAction : public Action { this->else_.add_action(new ContinuationAction(this)); } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; bool res = this->condition_->check(x...); if (res) { @@ -276,7 +294,7 @@ template class IfAction : public Action { } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { @@ -301,7 +319,7 @@ template class WhileAction : public Action { friend class WhileLoopContinuation; - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; // Initial condition check if (!this->condition_->check(x...)) { @@ -316,7 +334,7 @@ template class WhileAction : public Action { } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } @@ -327,7 +345,7 @@ template class WhileAction : public Action { }; // Implementation of WhileLoopContinuation::play -template void WhileLoopContinuation::play(Ts... x) { +template void WhileLoopContinuation::play(const Ts &...x) { if (this->parent_->num_running_ > 0 && this->parent_->condition_->check(x...)) { // play again this->parent_->then_.play(x...); @@ -346,7 +364,7 @@ template class RepeatLoopContinuation : public Action *parent) : parent_(parent) {} - void play(uint32_t iteration, Ts... x) override; + void play(const uint32_t &iteration, const Ts &...x) override; protected: RepeatAction *parent_; @@ -363,7 +381,7 @@ template class RepeatAction : public Action { friend class RepeatLoopContinuation; - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; if (this->count_.value(x...) > 0) { this->then_.play(0, x...); @@ -372,7 +390,7 @@ template class RepeatAction : public Action { } } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } @@ -382,12 +400,12 @@ template class RepeatAction : public Action { }; // Implementation of RepeatLoopContinuation::play -template void RepeatLoopContinuation::play(uint32_t iteration, Ts... x) { - iteration++; - if (iteration >= this->parent_->count_.value(x...)) { +template void RepeatLoopContinuation::play(const uint32_t &iteration, const Ts &...x) { + uint32_t next_iteration = iteration + 1; + if (next_iteration >= this->parent_->count_.value(x...)) { this->parent_->play_next_(x...); } else { - this->parent_->then_.play(iteration, x...); + this->parent_->then_.play(next_iteration, x...); } } @@ -406,10 +424,15 @@ template class WaitUntilAction : public Action, public Co void setup() override { // Start with loop disabled - only enable when there's work to do - this->disable_loop(); + // IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already + // called before our setup() (e.g., from on_boot trigger at same priority level) + // and we must not undo its enable_loop() call + if (this->num_running_ == 0) { + this->disable_loop(); + } } - void play_complex(Ts... x) override { + void play_complex(const Ts &...x) override { this->num_running_++; // Check if we can continue immediately. if (this->condition_->check(x...)) { @@ -424,34 +447,17 @@ template class WaitUntilAction : public Action, public Co auto timeout = this->timeout_value_.optional_value(x...); this->var_queue_.emplace_front(now, timeout, std::make_tuple(x...)); - // Enable loop now that we have work to do - this->enable_loop(); - this->loop(); + // Do immediate check with fresh timestamp + if (this->process_queue_(now)) { + // Only enable loop if we still have pending items + this->enable_loop(); + } } void loop() override { - if (this->num_running_ == 0) - return; - - auto now = App.get_loop_component_start_time(); - - this->var_queue_.remove_if([&](auto &queued) { - auto start = std::get(queued); - auto timeout = std::get>(queued); - auto &var = std::get>(queued); - - auto expired = timeout && (now - start) >= *timeout; - - if (!expired && !this->condition_->check_tuple(var)) { - return false; - } - - this->play_next_tuple_(var); - return true; - }); - - // If queue is now empty, disable loop until next play_complex - if (this->var_queue_.empty()) { + // Safe to use cached time - only called from Application::loop() + if (this->num_running_ > 0 && !this->process_queue_(App.get_loop_component_start_time())) { + // If queue is now empty, disable loop until next play_complex this->disable_loop(); } } @@ -463,10 +469,35 @@ template class WaitUntilAction : public Action, public Co float get_setup_priority() const override { return setup_priority::DATA; } - void play(Ts... x) override { /* ignore - see play_complex */ + void play(const Ts &...x) override { /* ignore - see play_complex */ } protected: + // Helper: Process queue, triggering completed items and removing them + // Returns true if queue still has pending items + bool process_queue_(uint32_t now) { + // Process each queued wait_until and remove completed ones + this->var_queue_.remove_if([&](auto &queued) { + auto start = std::get(queued); + auto timeout = std::get>(queued); + auto &var = std::get>(queued); + + // Check if timeout has expired + auto expired = timeout && (now - start) >= *timeout; + + // Keep waiting if not expired and condition not met + if (!expired && !this->condition_->check_tuple(var)) { + return false; + } + + // Condition met or timed out - trigger next action + this->play_next_tuple_(var); + return true; + }); + + return !this->var_queue_.empty(); + } + Condition *condition_; std::forward_list, std::tuple>> var_queue_{}; }; @@ -475,7 +506,7 @@ template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} - void play(Ts... x) override { + void play(const Ts &...x) override { if (!this->component_->is_ready()) return; this->component_->update(); @@ -489,7 +520,7 @@ template class SuspendComponentAction : public Action { public: SuspendComponentAction(PollingComponent *component) : component_(component) {} - void play(Ts... x) override { + void play(const Ts &...x) override { if (!this->component_->is_ready()) return; this->component_->stop_poller(); @@ -504,7 +535,7 @@ template class ResumeComponentAction : public Action { ResumeComponentAction(PollingComponent *component) : component_(component) {} TEMPLATABLE_VALUE(uint32_t, update_interval) - void play(Ts... x) override { + void play(const Ts &...x) override { if (!this->component_->is_ready()) { return; } diff --git a/esphome/core/config.py b/esphome/core/config.py index 27404538085..763f9ebd9fd 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -40,7 +40,12 @@ from esphome.const import ( PlatformFramework, __version__ as ESPHOME_VERSION, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import ( + CORE, + KEY_CONTROLLER_REGISTRY_COUNT, + CoroPriority, + coroutine_with_priority, +) from esphome.helpers import ( copy_file_if_changed, fnv1a_32bit_hash, @@ -462,6 +467,15 @@ async def _add_platform_defines() -> None: cg.add_define(f"USE_{platform_name.upper()}") +@coroutine_with_priority(CoroPriority.FINAL) +async def _add_controller_registry_define() -> None: + # Generate StaticVector size for ControllerRegistry + controller_count = CORE.data.get(KEY_CONTROLLER_REGISTRY_COUNT, 0) + if controller_count > 0: + cg.add_define("USE_CONTROLLER_REGISTRY") + cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) @@ -483,6 +497,7 @@ async def to_code(config: ConfigType) -> None: cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids)) CORE.add_job(_add_platform_defines) + CORE.add_job(_add_controller_registry_define) CORE.add_job(_add_automations, config) diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp deleted file mode 100644 index f7ff5a9734a..00000000000 --- a/esphome/core/controller.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include "controller.h" -#include "esphome/core/application.h" -#include "esphome/core/log.h" - -namespace esphome { - -void Controller::setup_controller(bool include_internal) { -#ifdef USE_BINARY_SENSOR - for (auto *obj : App.get_binary_sensors()) { - if (include_internal || !obj->is_internal()) { - obj->add_full_state_callback( - [this, obj](optional previous, optional state) { this->on_binary_sensor_update(obj); }); - } - } -#endif -#ifdef USE_FAN - for (auto *obj : App.get_fans()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_fan_update(obj); }); - } -#endif -#ifdef USE_LIGHT - for (auto *obj : App.get_lights()) { - if (include_internal || !obj->is_internal()) - obj->add_new_remote_values_callback([this, obj]() { this->on_light_update(obj); }); - } -#endif -#ifdef USE_SENSOR - for (auto *obj : App.get_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](float state) { this->on_sensor_update(obj, state); }); - } -#endif -#ifdef USE_SWITCH - for (auto *obj : App.get_switches()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](bool state) { this->on_switch_update(obj, state); }); - } -#endif -#ifdef USE_COVER - for (auto *obj : App.get_covers()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_cover_update(obj); }); - } -#endif -#ifdef USE_TEXT_SENSOR - for (auto *obj : App.get_text_sensors()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_sensor_update(obj, state); }); - } -#endif -#ifdef USE_CLIMATE - for (auto *obj : App.get_climates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](climate::Climate & /*unused*/) { this->on_climate_update(obj); }); - } -#endif -#ifdef USE_NUMBER - for (auto *obj : App.get_numbers()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); - } -#endif -#ifdef USE_DATETIME_DATE - for (auto *obj : App.get_dates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_date_update(obj); }); - } -#endif -#ifdef USE_DATETIME_TIME - for (auto *obj : App.get_times()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); }); - } -#endif -#ifdef USE_DATETIME_DATETIME - for (auto *obj : App.get_datetimes()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_datetime_update(obj); }); - } -#endif -#ifdef USE_TEXT - for (auto *obj : App.get_texts()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_update(obj, state); }); - } -#endif -#ifdef USE_SELECT - for (auto *obj : App.get_selects()) { - if (include_internal || !obj->is_internal()) { - obj->add_on_state_callback( - [this, obj](const std::string &state, size_t index) { this->on_select_update(obj, state, index); }); - } - } -#endif -#ifdef USE_LOCK - for (auto *obj : App.get_locks()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); }); - } -#endif -#ifdef USE_VALVE - for (auto *obj : App.get_valves()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_valve_update(obj); }); - } -#endif -#ifdef USE_MEDIA_PLAYER - for (auto *obj : App.get_media_players()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); }); - } -#endif -#ifdef USE_ALARM_CONTROL_PANEL - for (auto *obj : App.get_alarm_control_panels()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_alarm_control_panel_update(obj); }); - } -#endif -#ifdef USE_EVENT - for (auto *obj : App.get_events()) { - if (include_internal || !obj->is_internal()) - obj->add_on_event_callback([this, obj](const std::string &event_type) { this->on_event(obj, event_type); }); - } -#endif -#ifdef USE_UPDATE - for (auto *obj : App.get_updates()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj]() { this->on_update(obj); }); - } -#endif -} - -} // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index b475e326ee0..697017217db 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -69,7 +69,6 @@ namespace esphome { class Controller { public: - void setup_controller(bool include_internal = false); #ifdef USE_BINARY_SENSOR virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){}; #endif @@ -80,22 +79,22 @@ class Controller { virtual void on_light_update(light::LightState *obj){}; #endif #ifdef USE_SENSOR - virtual void on_sensor_update(sensor::Sensor *obj, float state){}; + virtual void on_sensor_update(sensor::Sensor *obj){}; #endif #ifdef USE_SWITCH - virtual void on_switch_update(switch_::Switch *obj, bool state){}; + virtual void on_switch_update(switch_::Switch *obj){}; #endif #ifdef USE_COVER virtual void on_cover_update(cover::Cover *obj){}; #endif #ifdef USE_TEXT_SENSOR - virtual void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state){}; + virtual void on_text_sensor_update(text_sensor::TextSensor *obj){}; #endif #ifdef USE_CLIMATE virtual void on_climate_update(climate::Climate *obj){}; #endif #ifdef USE_NUMBER - virtual void on_number_update(number::Number *obj, float state){}; + virtual void on_number_update(number::Number *obj){}; #endif #ifdef USE_DATETIME_DATE virtual void on_date_update(datetime::DateEntity *obj){}; @@ -107,10 +106,10 @@ class Controller { virtual void on_datetime_update(datetime::DateTimeEntity *obj){}; #endif #ifdef USE_TEXT - virtual void on_text_update(text::Text *obj, const std::string &state){}; + virtual void on_text_update(text::Text *obj){}; #endif #ifdef USE_SELECT - virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){}; + virtual void on_select_update(select::Select *obj){}; #endif #ifdef USE_LOCK virtual void on_lock_update(lock::Lock *obj){}; @@ -125,7 +124,7 @@ class Controller { virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){}; #endif #ifdef USE_EVENT - virtual void on_event(event::Event *obj, const std::string &event_type){}; + virtual void on_event(event::Event *obj){}; #endif #ifdef USE_UPDATE virtual void on_update(update::UpdateEntity *obj){}; diff --git a/esphome/core/controller_registry.cpp b/esphome/core/controller_registry.cpp new file mode 100644 index 00000000000..0a84bb0d0d5 --- /dev/null +++ b/esphome/core/controller_registry.cpp @@ -0,0 +1,114 @@ +#include "esphome/core/controller_registry.h" + +#ifdef USE_CONTROLLER_REGISTRY + +#include "esphome/core/controller.h" + +namespace esphome { + +StaticVector ControllerRegistry::controllers; + +void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); } + +// Macro for standard registry notification dispatch - calls on__update() +#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \ + void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + for (auto *controller : controllers) { \ + controller->on_##entity_name##_update(obj); \ + } \ + } + +// Macro for entities where controller method has no "_update" suffix (Event, Update) +#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \ + void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ + for (auto *controller : controllers) { \ + controller->on_##entity_name(obj); \ + } \ + } + +#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_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 new file mode 100644 index 00000000000..640a276a0a0 --- /dev/null +++ b/esphome/core/controller_registry.h @@ -0,0 +1,245 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_CONTROLLER_REGISTRY + +#include "esphome/core/helpers.h" + +// Forward declarations +namespace esphome { + +class Controller; + +#ifdef USE_BINARY_SENSOR +namespace binary_sensor { +class BinarySensor; +} +#endif + +#ifdef USE_FAN +namespace fan { +class Fan; +} +#endif + +#ifdef USE_LIGHT +namespace light { +class LightState; +} +#endif + +#ifdef USE_SENSOR +namespace sensor { +class Sensor; +} +#endif + +#ifdef USE_SWITCH +namespace switch_ { +class Switch; +} +#endif + +#ifdef USE_COVER +namespace cover { +class Cover; +} +#endif + +#ifdef USE_TEXT_SENSOR +namespace text_sensor { +class TextSensor; +} +#endif + +#ifdef USE_CLIMATE +namespace climate { +class Climate; +} +#endif + +#ifdef USE_NUMBER +namespace number { +class Number; +} +#endif + +#ifdef USE_DATETIME_DATE +namespace datetime { +class DateEntity; +} +#endif + +#ifdef USE_DATETIME_TIME +namespace datetime { +class TimeEntity; +} +#endif + +#ifdef USE_DATETIME_DATETIME +namespace datetime { +class DateTimeEntity; +} +#endif + +#ifdef USE_TEXT +namespace text { +class Text; +} +#endif + +#ifdef USE_SELECT +namespace select { +class Select; +} +#endif + +#ifdef USE_LOCK +namespace lock { +class Lock; +} +#endif + +#ifdef USE_VALVE +namespace valve { +class Valve; +} +#endif + +#ifdef USE_MEDIA_PLAYER +namespace media_player { +class MediaPlayer; +} +#endif + +#ifdef USE_ALARM_CONTROL_PANEL +namespace alarm_control_panel { +class AlarmControlPanel; +} +#endif + +#ifdef USE_EVENT +namespace event { +class Event; +} +#endif + +#ifdef USE_UPDATE +namespace update { +class UpdateEntity; +} +#endif + +/** Global registry for Controllers to receive entity state updates. + * + * This singleton registry allows Controllers (APIServer, WebServer) to receive + * entity state change notifications without storing per-entity callbacks. + * + * Instead of each entity maintaining controller callbacks (32 bytes overhead per entity), + * entities call ControllerRegistry::notify_*_update() which iterates the small list + * of registered controllers (typically 2: API and WebServer). + * + * Controllers read state directly from entities using existing accessors (obj->state, etc.) + * rather than receiving it as callback parameters that were being ignored anyway. + * + * Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead) + * Typical config (25 entities): ~780 bytes saved + * Large config (80 entities): ~2,540 bytes saved + */ +class ControllerRegistry { + public: + /** Register a controller to receive entity state updates. + * + * Controllers should call this in their setup() method. + * Typically only APIServer and WebServer register. + */ + static void register_controller(Controller *controller); + +#ifdef USE_BINARY_SENSOR + static void notify_binary_sensor_update(binary_sensor::BinarySensor *obj); +#endif + +#ifdef USE_FAN + static void notify_fan_update(fan::Fan *obj); +#endif + +#ifdef USE_LIGHT + static void notify_light_update(light::LightState *obj); +#endif + +#ifdef USE_SENSOR + static void notify_sensor_update(sensor::Sensor *obj); +#endif + +#ifdef USE_SWITCH + static void notify_switch_update(switch_::Switch *obj); +#endif + +#ifdef USE_COVER + static void notify_cover_update(cover::Cover *obj); +#endif + +#ifdef USE_TEXT_SENSOR + static void notify_text_sensor_update(text_sensor::TextSensor *obj); +#endif + +#ifdef USE_CLIMATE + static void notify_climate_update(climate::Climate *obj); +#endif + +#ifdef USE_NUMBER + static void notify_number_update(number::Number *obj); +#endif + +#ifdef USE_DATETIME_DATE + static void notify_date_update(datetime::DateEntity *obj); +#endif + +#ifdef USE_DATETIME_TIME + static void notify_time_update(datetime::TimeEntity *obj); +#endif + +#ifdef USE_DATETIME_DATETIME + static void notify_datetime_update(datetime::DateTimeEntity *obj); +#endif + +#ifdef USE_TEXT + static void notify_text_update(text::Text *obj); +#endif + +#ifdef USE_SELECT + static void notify_select_update(select::Select *obj); +#endif + +#ifdef USE_LOCK + static void notify_lock_update(lock::Lock *obj); +#endif + +#ifdef USE_VALVE + static void notify_valve_update(valve::Valve *obj); +#endif + +#ifdef USE_MEDIA_PLAYER + static void notify_media_player_update(media_player::MediaPlayer *obj); +#endif + +#ifdef USE_ALARM_CONTROL_PANEL + static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj); +#endif + +#ifdef USE_EVENT + static void notify_event(event::Event *obj); +#endif + +#ifdef USE_UPDATE + static void notify_update(update::UpdateEntity *obj); +#endif + + protected: + static StaticVector controllers; +}; + +} // namespace esphome + +#endif // USE_CONTROLLER_REGISTRY diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 868df6e2547..54818ffdaea 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -28,11 +28,13 @@ #define USE_BUTTON #define USE_CAMERA #define USE_CLIMATE +#define USE_CONTROLLER_REGISTRY #define USE_COVER #define USE_DATETIME #define USE_DATETIME_DATE #define USE_DATETIME_DATETIME #define USE_DATETIME_TIME +#define USE_DEBUG #define USE_DEEP_SLEEP #define USE_DEVICES #define USE_DISPLAY @@ -142,6 +144,7 @@ #define USE_TIME_TIMEZONE #define USE_WIFI #define USE_WIFI_AP +#define USE_WIFI_MANUAL_IP #define USE_WIREGUARD #endif @@ -155,6 +158,7 @@ // IDF-specific feature flags #ifdef USE_ESP_IDF #define USE_MQTT_IDF_ENQUEUE +#define ESPHOME_LOOP_TASK_STACK_SIZE 8192 #endif // ESP32-specific feature flags @@ -195,6 +199,7 @@ #define USE_PSRAM #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_SOCKET_SELECT_SUPPORT +#define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER #define USE_SPI #define USE_VOICE_ASSISTANT @@ -211,6 +216,7 @@ #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2) #define USE_ETHERNET #define USE_ETHERNET_KSZ8081 +#define USE_ETHERNET_MANUAL_IP #endif #ifdef USE_ESP_IDF @@ -283,6 +289,8 @@ #ifdef USE_NRF52 #define USE_NRF52_DFU +#define USE_NRF52_REG0_VOUT 5 +#define USE_NRF52_UICR_ERASE #define USE_SOFTDEVICE_ID 7 #define USE_SOFTDEVICE_VERSION 1 #endif @@ -294,6 +302,7 @@ #define USE_DASHBOARD_IMPORT // Default counts for static analysis +#define CONTROLLER_REGISTRY_MAX 2 #define ESPHOME_COMPONENT_COUNT 50 #define ESPHOME_DEVICE_COUNT 10 #define ESPHOME_AREA_COUNT 10 diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 80cd6b8e770..2b52d66f76e 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -61,6 +61,9 @@ class EntityBase { } // Get/set this entity's icon + ESPDEPRECATED( + "Use get_icon_ref() instead for better performance (avoids string copy). Will be removed in ESPHome 2026.5.0", + "2025.11.0") std::string get_icon() const; void set_icon(const char *icon); StringRef get_icon_ref() const { @@ -126,9 +129,6 @@ class EntityBase { // Returns empty StringRef if object_id is dynamic (needs allocation) StringRef get_object_id_ref_for_api_() const; - /// The hash_base() function has been deprecated. It is kept in this - /// class for now, to prevent external components from not compiling. - virtual uint32_t hash_base() { return 0L; } void calc_object_id_(); /// Check if the object_id is dynamic (changes with MAC suffix) @@ -158,6 +158,9 @@ class EntityBase { class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) public: /// Get the device class, using the manual override if set. + ESPDEPRECATED("Use get_device_class_ref() instead for better performance (avoids string copy). Will be removed in " + "ESPHome 2026.5.0", + "2025.11.0") std::string get_device_class(); /// Manually set the device class. void set_device_class(const char *device_class); @@ -174,6 +177,9 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) public: /// Get the unit of measurement, using the manual override if set. + ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be " + "removed in ESPHome 2026.5.0", + "2025.11.0") std::string get_unit_of_measurement(); /// Manually set the unit of measurement. void set_unit_of_measurement(const char *unit_of_measurement); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index aa94528e15a..7a0c458da00 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -641,6 +641,12 @@ std::string get_mac_address_pretty() { return format_mac_address_pretty(mac); } +void get_mac_address_into_buffer(std::span buf) { + uint8_t mac[6]; + get_mac_address_raw(mac); + format_mac_addr_lower_no_sep(mac, buf.data()); +} + #ifndef USE_ESP32 bool has_custom_mac_address() { return false; } #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 38ed2158ab8..c0dcce69a93 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -8,9 +8,11 @@ #include #include #include +#include #include #include #include +#include #include "esphome/core/optional.h" @@ -250,6 +252,8 @@ template class FixedVector { } // Allocate capacity - can be called multiple times to reinit + // IMPORTANT: After calling init(), you MUST use push_back() to add elements. + // Direct assignment via operator[] does NOT update the size counter. void init(size_t n) { cleanup_(); reset_(); @@ -868,6 +872,73 @@ template class CallbackManager { std::vector> callbacks_; }; +template class PartitionedCallbackManager; + +/** Helper class for callbacks partitioned into two sections. + * + * Uses a single vector partitioned into two sections: [first_0, ..., first_m-1, second_0, ..., second_n-1] + * The partition point is tracked externally by the caller (typically stored in the entity class for optimal alignment). + * + * Memory efficient: Only stores a single pointer (4 bytes on 32-bit platforms, 8 bytes on 64-bit platforms). + * The partition count lives in the entity class where it can be packed with other small fields to avoid padding waste. + * + * Design rationale: The asymmetric API (add_first takes first_count*, while call_first/call_second take it by value) + * is intentional - add_first must increment the count, while call methods only read it. This avoids storing first_count + * internally, saving memory per instance. + * + * @tparam Ts The arguments for the callbacks, wrapped in void(). + */ +template class PartitionedCallbackManager { + public: + /// Add a callback to the first partition. + void add_first(std::function &&callback, uint8_t *first_count) { + if (!this->callbacks_) { + this->callbacks_ = make_unique>>(); + } + + // Add to first partition: append then rotate into position + this->callbacks_->push_back(std::move(callback)); + // Avoid potential underflow: rewrite comparison to not subtract from size() + if (*first_count + 1 < this->callbacks_->size()) { + // Use std::rotate to maintain registration order in second partition + std::rotate(this->callbacks_->begin() + *first_count, this->callbacks_->end() - 1, this->callbacks_->end()); + } + (*first_count)++; + } + + /// Add a callback to the second partition. + void add_second(std::function &&callback) { + if (!this->callbacks_) { + this->callbacks_ = make_unique>>(); + } + + // Add to second partition: just append (already at end after first partition) + this->callbacks_->push_back(std::move(callback)); + } + + /// Call all callbacks in the first partition. + void call_first(uint8_t first_count, Ts... args) { + if (this->callbacks_) { + for (size_t i = 0; i < first_count; i++) { + (*this->callbacks_)[i](args...); + } + } + } + + /// Call all callbacks in the second partition. + void call_second(uint8_t first_count, Ts... args) { + if (this->callbacks_) { + for (size_t i = first_count; i < this->callbacks_->size(); i++) { + (*this->callbacks_)[i](args...); + } + } + } + + protected: + /// Partitioned callback storage: [first_0, ..., first_m-1, second_0, ..., second_n-1] + std::unique_ptr>> callbacks_; +}; + /// Helper class to deduplicate items in a series of values. template class Deduplicator { public: @@ -1030,6 +1101,10 @@ std::string get_mac_address(); /// Get the device MAC address as a string, in colon-separated uppercase hex notation. std::string get_mac_address_pretty(); +/// Get the device MAC address into the given buffer, in lowercase hex notation. +/// Assumes buffer length is 13 (12 digits for hexadecimal representation followed by null terminator). +void get_mac_address_into_buffer(std::span buf); + #ifdef USE_ESP32 /// Set the MAC address to use from the provided byte array (6 bytes). void set_mac_address(uint8_t *mac); @@ -1165,7 +1240,26 @@ template class RAMAllocator { template using ExternalRAMAllocator = RAMAllocator; -/// @} +/** + * Functions to constrain the range of arithmetic values. + */ + +template +concept comparable_with = requires(T a, U b) { + { a > b } -> std::convertible_to; + { a < b } -> std::convertible_to; +}; + +template U> T clamp_at_least(T value, U min) { + if (value < min) + return min; + return value; +} +template U> T clamp_at_most(T value, U max) { + if (value > max) + return max; + return value; +} /// @name Internal functions ///@{ diff --git a/esphome/core/macros.h b/esphome/core/macros.h index 8b2383321b2..2e47453c406 100644 --- a/esphome/core/macros.h +++ b/esphome/core/macros.h @@ -6,3 +6,7 @@ #ifdef USE_ARDUINO #include #endif + +#ifdef USE_ZEPHYR +#define M_PI 3.14159265358979323846 +#endif diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 11d59c24993..d285af2d0e1 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -117,12 +117,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->set_name(name_cstr, !is_static_string); item->type = type; item->callback = std::move(func); - // Initialize remove to false (though it should already be from constructor) -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - item->remove.store(false, std::memory_order_relaxed); -#else - item->remove = false; -#endif + // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use + this->set_item_removed_(item.get(), false); item->is_retry = is_retry; #ifndef ESPHOME_THREAD_SINGLE @@ -153,21 +149,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #ifdef ESPHOME_DEBUG_SCHEDULER - // Validate static strings in debug mode - if (is_static_string && name_cstr != nullptr) { - validate_static_string(name_cstr); - } - - // Debug logging - const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; - if (type == SchedulerItem::TIMEOUT) { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), - name_cstr ? name_cstr : "(null)", type_str, delay); - } else { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), - name_cstr ? name_cstr : "(null)", type_str, delay, - static_cast(item->get_next_execution() - now)); - } + this->debug_log_timer_(item.get(), is_static_string, name_cstr, type, delay, now); #endif /* ESPHOME_DEBUG_SCHEDULER */ // For retries, check if there's a cancelled timeout first @@ -787,4 +769,25 @@ void Scheduler::recycle_item_(std::unique_ptr item) { // else: unique_ptr will delete the item when it goes out of scope } +#ifdef ESPHOME_DEBUG_SCHEDULER +void Scheduler::debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, + SchedulerItem::Type type, uint32_t delay, uint64_t now) { + // Validate static strings in debug mode + if (is_static_string && name_cstr != nullptr) { + validate_static_string(name_cstr); + } + + // Debug logging + const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; + if (type == SchedulerItem::TIMEOUT) { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), + name_cstr ? name_cstr : "(null)", type_str, delay); + } else { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), + name_cstr ? name_cstr : "(null)", type_str, delay, + static_cast(item->get_next_execution() - now)); + } +} +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index aa16a7a125a..c328691e90d 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -265,6 +265,12 @@ class Scheduler { // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); +#ifdef ESPHOME_DEBUG_SCHEDULER + // Helper for debug logging in set_timer_common_ - extracted to reduce code size + void debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, + SchedulerItem::Type type, uint32_t delay, uint64_t now); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + #ifndef ESPHOME_THREAD_SINGLE // Helper to process defer queue - inline for performance in hot path inline void process_defer_queue_(uint32_t &now) { @@ -366,6 +372,24 @@ class Scheduler { #endif } + // Helper to set item removal flag (platform-specific) + // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this + // function. Uses memory_order_release when setting to true (for cancellation synchronization), + // and memory_order_relaxed when setting to false (for initialization). + void set_item_removed_(SchedulerItem *item, bool removed) { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Multi-threaded with atomics: use atomic store with appropriate ordering + // Release ordering when setting to true ensures cancellation is visible to other threads + // Relaxed ordering when setting to false is sufficient for initialization + item->remove.store(removed, removed ? std::memory_order_release : std::memory_order_relaxed); +#else + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + item->remove = removed; +#endif + } + // Helper to mark matching items in a container as removed // Returns the number of items marked for removal // IMPORTANT: Caller must hold the scheduler lock before calling this function. @@ -382,15 +406,7 @@ class Scheduler { continue; if (this->matches_item_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - // Multi-threaded with atomics: use atomic store - item->remove.store(true, std::memory_order_release); -#else - // Single-threaded (ESPHOME_THREAD_SINGLE) or - // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write - // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! - item->remove = true; -#endif + this->set_item_removed_(item.get(), true); count++; } } diff --git a/esphome/core/time.h b/esphome/core/time.h index ffcfced418c..68826dabdc2 100644 --- a/esphome/core/time.h +++ b/esphome/core/time.h @@ -84,6 +84,9 @@ struct ESPTime { */ static ESPTime from_epoch_local(time_t epoch) { struct tm *c_tm = ::localtime(&epoch); + if (c_tm == nullptr) { + return ESPTime{}; // Return an invalid ESPTime + } return ESPTime::from_c_tm(c_tm, epoch); } /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. @@ -93,6 +96,9 @@ struct ESPTime { */ static ESPTime from_epoch_utc(time_t epoch) { struct tm *c_tm = ::gmtime(&epoch); + if (c_tm == nullptr) { + return ESPTime{}; // Return an invalid ESPTime + } return ESPTime::from_c_tm(c_tm, epoch); } diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 6f1af01a5be..2904cae7966 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -19,11 +19,16 @@ from esphome.core import ( TimePeriodNanoseconds, TimePeriodSeconds, ) +from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last from esphome.types import Expression, SafeExpType, TemplateArgsType from esphome.util import OrderedDict from esphome.yaml_util import ESPHomeDataBase +# Keys for lambda deduplication storage in CORE.data +_KEY_LAMBDA_DEDUP = "lambda_dedup" +_KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations" + class RawExpression(Expression): __slots__ = ("text",) @@ -188,7 +193,7 @@ class LambdaExpression(Expression): def __init__( self, parts, parameters, capture: str = "=", return_type=None, source=None - ): + ) -> None: self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) @@ -197,16 +202,21 @@ class LambdaExpression(Expression): self.capture = capture self.return_type = safe_exp(return_type) if return_type is not None else None - def __str__(self): + def format_body(self) -> str: + """Format the lambda body with source directive and content.""" + body = "" + if self.source is not None: + body += f"{self.source.as_line_directive}\n" + body += self.content + return body + + def __str__(self) -> str: # Stateless lambdas (empty capture) implicitly convert to function pointers # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" - cpp += " {\n" - if self.source is not None: - cpp += f"{self.source.as_line_directive}\n" - cpp += f"{self.content}\n}}" + cpp += f" {{\n{self.format_body()}\n}}" return indent_all_but_first_and_last(cpp) @property @@ -214,6 +224,37 @@ class LambdaExpression(Expression): return "".join(str(part) for part in self.parts) +class SharedFunctionLambdaExpression(LambdaExpression): + """A lambda expression that references a shared deduplicated function. + + This class wraps a function pointer but maintains the LambdaExpression + interface so calling code works unchanged. + """ + + __slots__ = ("_func_name",) + + def __init__( + self, + func_name: str, + parameters: TemplateArgsType, + return_type: SafeExpType | None = None, + ) -> None: + # Initialize parent with empty parts since we're just a function reference + super().__init__( + [], parameters, capture="", return_type=return_type, source=None + ) + self._func_name = func_name + + def __str__(self) -> str: + # Just return the function name - it's already a function pointer + return self._func_name + + @property + def content(self) -> str: + # No content, just a function reference + return "" + + # pylint: disable=abstract-method class Literal(Expression, metaclass=abc.ABCMeta): __slots__ = () @@ -583,6 +624,25 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False): CORE.add_global(expression, prepend) +@coroutine_with_priority(CoroPriority.FINAL) +async def flush_lambda_dedup_declarations() -> None: + """Flush all deferred lambda deduplication declarations to global scope. + + This is a coroutine that runs with FINAL priority (after all components) + to ensure all referenced variables are declared before the shared + lambda functions that use them. + """ + if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data: + return + + declarations = CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] + for func_declaration in declarations: + add_global(RawStatement(func_declaration)) + + # Clear the list so we don't add them again + CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = [] + + def add_library(name: str, version: str | None, repository: str | None = None): """Add a library to the codegen library storage. @@ -656,6 +716,62 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: return await CORE.get_variable_with_full_id(id_) +def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str: + """Get the shared function name for a lambda expression. + + If an identical lambda was already generated, returns the existing shared + function name. Otherwise, creates a new shared function and returns its name. + + Args: + lambda_expr: The lambda expression to deduplicate + + Returns: + The name of the shared function for this lambda (either existing or newly created) + """ + # Create a unique key from the lambda content, parameters, and return type + content = lambda_expr.content + param_str = str(lambda_expr.parameters) + return_str = ( + str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void" + ) + + # Use tuple of (content, params, return_type) as key + lambda_key = (content, param_str, return_str) + + # Initialize deduplication storage in CORE.data if not exists + if _KEY_LAMBDA_DEDUP not in CORE.data: + CORE.data[_KEY_LAMBDA_DEDUP] = {} + # Register the flush job to run after all components (FINAL priority) + # This ensures all variables are declared before shared lambda functions + CORE.add_job(flush_lambda_dedup_declarations) + + lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP] + + # Check if we've seen this lambda before + if lambda_key in lambda_cache: + # Return name of existing shared function + return lambda_cache[lambda_key] + + # First occurrence - create a shared function + # Use the cache size as the function number + func_name = f"shared_lambda_{len(lambda_cache)}" + + # Build the function declaration using lambda's body formatting + func_declaration = ( + f"{return_str} {func_name}({param_str}) {{\n{lambda_expr.format_body()}\n}}" + ) + + # Store the declaration to be added later (after all variable declarations) + # We can't add it immediately because it might reference variables not yet declared + CORE.data.setdefault(_KEY_LAMBDA_DEDUP_DECLARATIONS, []).append(func_declaration) + + # Store in cache + lambda_cache[lambda_key] = func_name + + # Return the function name (this is the first occurrence, but we still generate shared function) + return func_name + + async def process_lambda( value: Lambda, parameters: TemplateArgsType, @@ -713,6 +829,17 @@ async def process_lambda( location.line += value.content_offset else: location = None + + # Lambda deduplication: Only deduplicate stateless lambdas (empty capture). + # Stateful lambdas cannot be shared as they capture different contexts. + if capture == "": + lambda_expr = LambdaExpression( + parts, parameters, capture, return_type, location + ) + func_name = _get_shared_lambda_name(lambda_expr) + # Return a shared function reference instead of inline lambda + return SharedFunctionLambdaExpression(func_name, parameters, return_type) + return LambdaExpression(parts, parameters, capture, return_type, location) diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 1a6dc8b97d3..fcb3a4f4382 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -6,15 +6,15 @@ dependencies: espressif/mdns: version: 1.8.2 espressif/esp_wifi_remote: - version: 0.10.2 + version: 1.1.5 rules: - if: "target in [esp32h2, esp32p4]" espressif/eppp_link: - version: 0.2.0 + version: 1.1.3 rules: - if: "target in [esp32h2, esp32p4]" espressif/esp_hosted: - version: 2.0.11 + version: 2.6.1 rules: - if: "target in [esp32h2, esp32p4]" zorxx/multipart-parser: @@ -23,3 +23,7 @@ dependencies: version: "2.0.0" rules: - if: "target in [esp32, esp32p4]" + espressif/esp_tinyusb: + version: "1.7.6~1" + rules: + - if: "target in [esp32s2, esp32s3, esp32p4]" diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 093ee64df4d..0d50edbc2c7 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -30,6 +30,7 @@ from esphome.const import ( from esphome.core import CORE, EsphomeError from esphome.helpers import get_int_env, get_str_env from esphome.log import AnsiFore, color +from esphome.types import ConfigType from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -154,8 +155,12 @@ def show_discover(config, username=None, password=None, client_id=None): def get_esphome_device_ip( - config, username=None, password=None, client_id=None, timeout=25 -): + config: ConfigType, + username: str | None = None, + password: str | None = None, + client_id: str | None = None, + timeout: int | float = 25, +) -> list[str]: if CONF_MQTT not in config: raise EsphomeError( "Cannot discover IP via MQTT as the config does not include the mqtt: " @@ -166,6 +171,10 @@ def get_esphome_device_ip( "Cannot discover IP via MQTT as the config does not include the device name: " "component" ) + if not config[CONF_MQTT].get(CONF_BROKER): + raise EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) dev_name = config[CONF_ESPHOME][CONF_NAME] dev_ip = None diff --git a/requirements.txt b/requirements.txt index 660b18c933f..40802422f2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20251013.0 -aioesphomeapi==42.5.0 +aioesphomeapi==42.7.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.18.16 # dashboard_import diff --git a/requirements_test.txt b/requirements_test.txt index 11367172b11..e238faa77ec 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,14 +1,14 @@ -pylint==4.0.2 +pylint==4.0.3 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.3 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating +ruff==0.14.5 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==8.4.2 +pytest==9.0.1 pytest-cov==7.0.0 pytest-mock==3.15.1 -pytest-asyncio==1.2.0 +pytest-asyncio==1.3.0 pytest-xdist==3.8.0 asyncmock==0.4.2 hypothesis==6.92.1 diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 38d1f8c2b72..27a36f889f4 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -86,6 +86,7 @@ ISOLATED_COMPONENTS = { "modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus", "neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)", "packages": "cannot merge packages", + "tinyusb": "Conflicts with usb_host component - cannot be used together", } diff --git a/script/ci-custom.py b/script/ci-custom.py index 6b01623d92c..106aa438fe7 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -71,6 +71,7 @@ ignore_types = ( ".apng", ".gif", ".webp", + ".bin", ) LINT_FILE_CHECKS = [] diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 4a0edebb0dd..5cc3f2570aa 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -63,6 +63,7 @@ from helpers import ( get_components_from_integration_fixtures, get_components_with_dependencies, get_cpp_changed_components, + get_target_branch, git_ls_files, parse_test_filename, root_path, @@ -93,6 +94,23 @@ class Platform(StrEnum): # Memory impact analysis constants MEMORY_IMPACT_FALLBACK_COMPONENT = "api" # Representative component for core changes MEMORY_IMPACT_FALLBACK_PLATFORM = Platform.ESP32_IDF # Most representative platform +MEMORY_IMPACT_MAX_COMPONENTS = 40 # Max components before results become nonsensical + +# Platform-specific components that can only be built on their respective platforms +# These components contain platform-specific code and cannot be cross-compiled +# Regular components (wifi, logger, api, etc.) are cross-platform and not listed here +PLATFORM_SPECIFIC_COMPONENTS = frozenset( + { + "esp32", # ESP32 platform implementation + "esp8266", # ESP8266 platform implementation + "rp2040", # Raspberry Pi Pico / RP2040 platform implementation + "bk72xx", # Beken BK72xx platform implementation (uses LibreTiny) + "rtl87xx", # Realtek RTL87xx platform implementation (uses LibreTiny) + "ln882x", # Winner Micro LN882x platform implementation (uses LibreTiny) + "host", # Host platform (for testing on development machine) + "nrf52", # Nordic nRF52 platform implementation + } +) # Platform preference order for memory impact analysis # This order is used when no platform-specific hints are detected from filenames @@ -455,6 +473,20 @@ def detect_memory_impact_config( - platform: platform name for the merged build - use_merged_config: "true" (always use merged config) """ + # Skip memory impact analysis for release* or beta* branches + # These branches typically contain many merged changes from dev, and building + # all components at once would produce nonsensical memory impact results. + # Memory impact analysis is most useful for focused PRs targeting dev. + target_branch = get_target_branch() + if target_branch and ( + target_branch.startswith("release") or target_branch.startswith("beta") + ): + print( + f"Memory impact: Skipping analysis for target branch {target_branch} " + f"(would try to build all components at once, giving nonsensical results)", + file=sys.stderr, + ) + return {"should_run": "false"} # Get actually changed files (not dependencies) files = changed_files(branch) @@ -524,6 +556,17 @@ def detect_memory_impact_config( if not components_with_tests: return {"should_run": "false"} + # Skip memory impact analysis if too many components changed + # Building 40+ components at once produces nonsensical memory impact results + # This typically happens with large refactorings or batch updates + if len(components_with_tests) > MEMORY_IMPACT_MAX_COMPONENTS: + print( + f"Memory impact: Skipping analysis for {len(components_with_tests)} components " + f"(limit is {MEMORY_IMPACT_MAX_COMPONENTS}, would give nonsensical results)", + file=sys.stderr, + ) + return {"should_run": "false"} + # Find common platforms supported by ALL components # This ensures we can build all components together in a merged config common_platforms = set(MEMORY_IMPACT_PLATFORM_PREFERENCE) @@ -568,6 +611,20 @@ def detect_memory_impact_config( ) platform = _select_platform_by_count(platform_counts) + # Filter out platform-specific components that are incompatible with selected platform + # Platform components (esp32, esp8266, rp2040, etc.) can only build on their own platform + # Other components (wifi, logger, etc.) are cross-platform and can build anywhere + compatible_components = [ + component + for component in components_with_tests + if component not in PLATFORM_SPECIFIC_COMPONENTS + or platform in component_platforms_map.get(component, set()) + ] + + # If no components are compatible with the selected platform, don't run + if not compatible_components: + return {"should_run": "false"} + # Debug output print("Memory impact analysis:", file=sys.stderr) print(f" Changed components: {sorted(changed_component_set)}", file=sys.stderr) @@ -579,10 +636,11 @@ def detect_memory_impact_config( print(f" Platform hints from filenames: {platform_hints}", file=sys.stderr) print(f" Common platforms: {sorted(common_platforms)}", file=sys.stderr) print(f" Selected platform: {platform}", file=sys.stderr) + print(f" Compatible components: {compatible_components}", file=sys.stderr) return { "should_run": "true", - "components": components_with_tests, + "components": compatible_components, "platform": platform, "use_merged_config": "true", } @@ -698,11 +756,27 @@ def main() -> None: component_test_batches: list[str] if changed_components_with_tests: tests_dir = Path(root_path) / ESPHOME_TESTS_COMPONENTS_PATH + + # For beta/release branches, group all components for faster CI + # (no isolation, all components are groupable) + target_branch = get_target_branch() + is_release_branch = target_branch and ( + target_branch.startswith("release") or target_branch.startswith("beta") + ) + + if is_release_branch: + # For beta/release: Don't isolate any components - group everything + # This allows components to be merged into single builds + batch_directly_changed = set() # Empty set - no isolation + else: + # Normal PR: only directly changed components are isolated + batch_directly_changed = directly_changed_with_tests + batches, _ = create_intelligent_batches( components=changed_components_with_tests, tests_dir=tests_dir, batch_size=COMPONENT_TEST_BATCH_SIZE, - directly_changed=directly_changed_with_tests, + directly_changed=batch_directly_changed, ) # Convert batches to space-separated strings for CI matrix component_test_batches = [" ".join(batch) for batch in batches] diff --git a/script/helpers.py b/script/helpers.py index 447d54fa54f..1039ef39ac2 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable from functools import cache +import hashlib import json import os import os.path @@ -52,6 +53,10 @@ BASE_BUS_COMPONENTS = { "remote_receiver", } +# Cache version for components graph +# Increment this when the cache format or graph building logic changes +COMPONENTS_GRAPH_CACHE_VERSION = 1 + def parse_list_components_output(output: str) -> list[str]: """Parse the output from list-components.py script. @@ -101,7 +106,11 @@ def get_component_from_path(file_path: str) -> str | None: ): parts = file_path.split("/") if len(parts) >= 3 and parts[2]: - return parts[2] + # Verify that parts[2] is actually a component directory, not a file + # like .gitignore or README.md in the components directory itself + component_name = parts[2] + if "." not in component_name: + return component_name return None @@ -187,6 +196,20 @@ def splitlines_no_ends(string: str) -> list[str]: return [s.strip() for s in string.splitlines()] +@cache +def _get_github_event_data() -> dict | None: + """Read and parse GitHub event file (cached). + + Returns: + Parsed event data dictionary, or None if not available + """ + github_event_path = os.environ.get("GITHUB_EVENT_PATH") + if github_event_path and os.path.exists(github_event_path): + with open(github_event_path) as f: + return json.load(f) + return None + + def _get_pr_number_from_github_env() -> str | None: """Extract PR number from GitHub environment variables. @@ -199,13 +222,30 @@ def _get_pr_number_from_github_env() -> str | None: return github_ref.split("/pull/")[1].split("/")[0] # Fallback to GitHub event file - github_event_path = os.environ.get("GITHUB_EVENT_PATH") - if github_event_path and os.path.exists(github_event_path): - with open(github_event_path) as f: - event_data = json.load(f) - pr_data = event_data.get("pull_request", {}) - if pr_number := pr_data.get("number"): - return str(pr_number) + if event_data := _get_github_event_data(): + pr_data = event_data.get("pull_request", {}) + if pr_number := pr_data.get("number"): + return str(pr_number) + + return None + + +def get_target_branch() -> str | None: + """Get the target branch from GitHub environment variables. + + Returns: + Target branch name (e.g., "dev", "release", "beta"), or None if not in PR context + """ + # First try GITHUB_BASE_REF (set for pull_request events) + if base_ref := os.environ.get("GITHUB_BASE_REF"): + return base_ref + + # Fallback to GitHub event file + if event_data := _get_github_event_data(): + pr_data = event_data.get("pull_request", {}) + base_data = pr_data.get("base", {}) + if ref := base_data.get("ref"): + return ref return None @@ -752,20 +792,71 @@ def resolve_auto_load( return auto_load() +@cache +def get_components_graph_cache_key() -> str: + """Generate cache key based on all component Python file hashes. + + Uses git ls-files with sha1 hashes to generate a stable cache key that works + across different machines and CI runs. This is faster and more reliable than + reading file contents or using modification times. + + Returns: + SHA256 hex string uniquely identifying the current component state + """ + + # Use git ls-files -s to get sha1 hashes of all component Python files + # Format: + # This is fast and works consistently across CI and local dev + # We hash all .py files because AUTO_LOAD, DEPENDENCIES, etc. can be defined + # in any Python file, not just __init__.py + cmd = ["git", "ls-files", "-s", "esphome/components/**/*.py"] + result = subprocess.run( + cmd, capture_output=True, text=True, check=True, cwd=root_path, close_fds=False + ) + + # Hash the git output (includes file paths and their sha1 hashes) + # This changes only when component Python files actually change + hasher = hashlib.sha256() + hasher.update(result.stdout.encode()) + + return hasher.hexdigest() + + def create_components_graph() -> dict[str, list[str]]: - """Create a graph of component dependencies. + """Create a graph of component dependencies (cached). + + This function is expensive (5-6 seconds) because it imports all ESPHome components + to extract their DEPENDENCIES and AUTO_LOAD metadata. The result is cached based + on component file modification times, so unchanged components don't trigger a rebuild. Returns: Dictionary mapping parent components to their children (dependencies) """ - from pathlib import Path + # Check cache first - use fixed filename since GitHub Actions cache doesn't support wildcards + cache_file = Path(temp_folder) / "components_graph.json" + + if cache_file.exists(): + try: + cached_data = json.loads(cache_file.read_text()) + except (OSError, json.JSONDecodeError): + # Cache file corrupted or unreadable, rebuild + pass + else: + # Verify cache version matches + if cached_data.get("_version") == COMPONENTS_GRAPH_CACHE_VERSION: + # Verify cache is for current component state + cache_key = get_components_graph_cache_key() + if cached_data.get("_cache_key") == cache_key: + return cached_data.get("graph", {}) + # Cache key mismatch - stale cache, rebuild + # Cache version mismatch - incompatible format, rebuild from esphome import const from esphome.core import CORE from esphome.loader import ComponentManifest, get_component, get_platform # The root directory of the repo - root = Path(__file__).parent.parent + root = Path(root_path) components_dir = root / ESPHOME_COMPONENTS_PATH # Fake some directory so that get_component works CORE.config_path = root @@ -842,6 +933,15 @@ def create_components_graph() -> dict[str, list[str]]: # restore config CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + # Save to cache with version and cache key for validation + cache_data = { + "_version": COMPONENTS_GRAPH_CACHE_VERSION, + "_cache_key": get_components_graph_cache_key(), + "graph": components_graph, + } + cache_file.parent.mkdir(exist_ok=True) + cache_file.write_text(json.dumps(cache_data)) + return components_graph diff --git a/tests/component_tests/ota/test_web_server_ota.py b/tests/component_tests/ota/test_web_server_ota.py index 0d8ff6f134f..d4630ff203d 100644 --- a/tests/component_tests/ota/test_web_server_ota.py +++ b/tests/component_tests/ota/test_web_server_ota.py @@ -1,6 +1,21 @@ """Tests for the web_server OTA platform.""" +from __future__ import annotations + from collections.abc import Callable +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.web_server.ota import ( + CONF_WEB_SERVER, + _web_server_ota_final_validate, +) +from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM +from esphome.core import ID +import esphome.final_validate as fv def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None: @@ -100,3 +115,111 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None: # Check web server OTA component is present assert "WebServerOTAComponent" in main_cpp assert "web_server::WebServerOTAComponent" in main_cpp + + +@pytest.mark.parametrize( + ("ota_configs", "expected_count", "warning_expected"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web", is_manual=False), + } + ], + 1, + False, + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 1, + True, + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=False), + }, + { + CONF_PLATFORM: "esphome", + CONF_ID: ID("ota_esphome", is_manual=False), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=False), + }, + ], + 2, + True, + id="mixed_platforms_web_server_merged", + ), + ], +) +def test_web_server_ota_instance_merging( + ota_configs: list[dict[str, Any]], + expected_count: int, + warning_expected: bool, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test web_server OTA instance merging behavior.""" + full_conf = {CONF_OTA: ota_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _web_server_ota_final_validate({}) + + updated_conf = fv.full_config.get() + + # Verify total number of OTA platforms + assert len(updated_conf[CONF_OTA]) == expected_count + + # Verify warning + if warning_expected: + assert any( + "Found and merged" in record.message + and "web_server OTA" in record.message + for record in caplog.records + ), "Expected merge warning not found in log" + else: + assert len(caplog.records) == 0, "Unexpected warnings logged" + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + ota_configs = [ + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_1", is_manual=True), + }, + { + CONF_PLATFORM: CONF_WEB_SERVER, + CONF_ID: ID("ota_web_2", is_manual=True), + }, + ] + + full_conf = {CONF_OTA: ota_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple web_server OTA configurations but id is inconsistent", + ): + _web_server_ota_final_validate({}) + finally: + fv.full_config.reset(token) diff --git a/tests/component_tests/psram/test_psram.py b/tests/component_tests/psram/test_psram.py index 3e40a8d1920..f8ad013689b 100644 --- a/tests/component_tests/psram/test_psram.py +++ b/tests/component_tests/psram/test_psram.py @@ -34,6 +34,12 @@ SUPPORTED_PSRAM_VARIANTS = [ VARIANT_ESP32S3, VARIANT_ESP32P4, ] +SUPPORTED_PSRAM_MODES = { + VARIANT_ESP32: ["quad"], + VARIANT_ESP32S2: ["quad"], + VARIANT_ESP32S3: ["quad", "octal"], + VARIANT_ESP32P4: ["hex"], +} @pytest.mark.parametrize( @@ -86,7 +92,7 @@ def test_psram_configuration_valid_supported_variants( from esphome.components.psram import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA # This should not raise an exception - config = CONFIG_SCHEMA({}) + config = CONFIG_SCHEMA({"mode": SUPPORTED_PSRAM_MODES[variant][0]}) FINAL_VALIDATE_SCHEMA(config) @@ -122,7 +128,7 @@ def _setup_psram_final_validation_test( ("config", "esp32_config", "expect_error", "error_match"), [ pytest.param( - {"speed": "120MHz"}, + {"mode": "quad", "speed": "120MHz"}, {"cpu_frequency": "160MHz"}, True, r"PSRAM 120MHz requires 240MHz CPU frequency", @@ -143,7 +149,7 @@ def _setup_psram_final_validation_test( id="ecc_only_in_octal_mode", ), pytest.param( - {"speed": "120MHZ"}, + {"mode": "quad", "speed": "120MHZ"}, {"cpu_frequency": "240MHZ"}, False, None, diff --git a/tests/component_tests/sntp/__init__.py b/tests/component_tests/sntp/__init__.py new file mode 100644 index 00000000000..7d323a49808 --- /dev/null +++ b/tests/component_tests/sntp/__init__.py @@ -0,0 +1 @@ +"""Tests for SNTP component.""" diff --git a/tests/component_tests/sntp/config/sntp_test.yaml b/tests/component_tests/sntp/config/sntp_test.yaml new file mode 100644 index 00000000000..3942c9606bb --- /dev/null +++ b/tests/component_tests/sntp/config/sntp_test.yaml @@ -0,0 +1,22 @@ +esphome: + name: sntp-test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "testssid" + password: "testpassword" + +# Test multiple SNTP instances that should be merged +time: + - platform: sntp + servers: + - 192.168.1.1 + - pool.ntp.org + - platform: sntp + servers: + - pool.ntp.org + - 192.168.1.2 diff --git a/tests/component_tests/sntp/test_init.py b/tests/component_tests/sntp/test_init.py new file mode 100644 index 00000000000..9197ff55d0a --- /dev/null +++ b/tests/component_tests/sntp/test_init.py @@ -0,0 +1,238 @@ +"""Tests for SNTP time configuration validation.""" + +from __future__ import annotations + +import logging +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate +from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME +from esphome.core import ID +import esphome.final_validate as fv + + +@pytest.mark.parametrize( + ("time_configs", "expected_count", "expected_servers", "warning_messages"), + [ + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + } + ], + 1, + ["192.168.1.1", "pool.ntp.org"], + [], + id="single_instance_no_merge", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="two_instances_merged", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_preserves_order", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_3", is_manual=False), + CONF_SERVERS: ["pool3.ntp.org"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + [ + "SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']", + "Found and merged 3 SNTP time configurations into one instance", + ], + id="three_instances_drops_excess_servers", + ), + pytest.param( + [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: [ + "192.168.1.1", + "pool.ntp.org", + "pool.ntp.org", + "192.168.1.1", + ], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"], + }, + ], + 1, + ["192.168.1.1", "pool.ntp.org", "192.168.1.2"], + ["Found and merged 2 SNTP time configurations into one instance"], + id="deduplication_multiple_duplicates", + ), + ], +) +def test_sntp_instance_merging( + time_configs: list[dict[str, Any]], + expected_count: int, + expected_servers: list[str], + warning_messages: list[str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test SNTP instance merging behavior.""" + # Create a mock full config with time configs + full_conf = {CONF_TIME: time_configs.copy()} + + # Set the context var + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + # Get the updated config + updated_conf = fv.full_config.get() + + # Check if merging occurred + if len(time_configs) > 1: + # Verify only one SNTP instance remains + sntp_instances = [ + tc + for tc in updated_conf[CONF_TIME] + if tc.get(CONF_PLATFORM) == CONF_SNTP + ] + assert len(sntp_instances) == expected_count + + # Verify server list + assert sntp_instances[0][CONF_SERVERS] == expected_servers + + # Verify warnings + for expected_msg in warning_messages: + assert any( + expected_msg in record.message for record in caplog.records + ), f"Expected warning message '{expected_msg}' not found in log" + else: + # Single instance should not trigger merging or warnings + assert len(caplog.records) == 0 + # Config should be unchanged + assert updated_conf[CONF_TIME] == time_configs + finally: + fv.full_config.reset(token) + + +def test_sntp_inconsistent_manual_ids() -> None: + """Test that inconsistent manual IDs raise an error.""" + # Create configs with manual IDs that are inconsistent + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=True), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=True), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs} + + token = fv.full_config.set(full_conf) + try: + with pytest.raises( + cv.Invalid, + match="Found multiple SNTP configurations but id is inconsistent", + ): + _sntp_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None: + """Test that SNTP merging doesn't affect other time platforms.""" + time_configs = [ + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_1", is_manual=False), + CONF_SERVERS: ["192.168.1.1"], + }, + { + CONF_PLATFORM: "homeassistant", + CONF_ID: ID("homeassistant_time", is_manual=False), + }, + { + CONF_PLATFORM: CONF_SNTP, + CONF_ID: ID("sntp_time_2", is_manual=False), + CONF_SERVERS: ["192.168.1.2"], + }, + ] + + full_conf = {CONF_TIME: time_configs.copy()} + + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + _sntp_final_validate({}) + + updated_conf = fv.full_config.get() + + # Should have 2 time platforms: 1 merged SNTP + 1 homeassistant + assert len(updated_conf[CONF_TIME]) == 2 + + # Find the platforms + platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]} + assert platforms == {CONF_SNTP, "homeassistant"} + + # Verify SNTP was merged + sntp_instances = [ + tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP + ] + assert len(sntp_instances) == 1 + assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"] + finally: + fv.full_config.reset(token) diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 1cc31a288b0..ffc0fd780ae 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -1,4 +1,6 @@ -"""Tests for the binary sensor component.""" +"""Tests for the text component.""" + +from esphome.core import CORE def test_text_is_setup(generate_main): @@ -56,16 +58,22 @@ def test_text_config_value_mode_set(generate_main): assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp -def test_text_config_lamda_is_set(generate_main): +def test_text_config_lambda_is_set(generate_main) -> None: """ - Test if lambda is set for lambda mode (optimized with stateless lambda) + Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication) """ # Given # When main_cpp = generate_main("tests/component_tests/text/test_text.yaml") + # Get both global and main sections to find the shared lambda definition + full_cpp = CORE.cpp_global_section + main_cpp + # Then - # Stateless lambda optimization: empty capture list allows function pointer conversion - assert "it_4->set_template([]() -> esphome::optional {" in main_cpp - assert 'return std::string{"Hello"};' in main_cpp + # Lambda is deduplicated into a shared function (reference in main section) + assert "it_4->set_template(shared_lambda_" in main_cpp + # Lambda body should be in the code somewhere + assert 'return std::string{"Hello"};' in full_cpp + # Verify the shared lambda function is defined (in global section) + assert "esphome::optional shared_lambda_" in full_cpp diff --git a/tests/components/a01nyub/test.esp32-c3-idf.yaml b/tests/components/a01nyub/test.esp32-c3-idf.yaml deleted file mode 100644 index 2cda8deaf9f..00000000000 --- a/tests/components/a01nyub/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a02yyuw/test.esp32-c3-idf.yaml b/tests/components/a02yyuw/test.esp32-c3-idf.yaml deleted file mode 100644 index 2cda8deaf9f..00000000000 --- a/tests/components/a02yyuw/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/a4988/test.esp32-c3-idf.yaml b/tests/components/a4988/test.esp32-c3-idf.yaml deleted file mode 100644 index 25caba75b5e..00000000000 --- a/tests/components/a4988/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - step_pin: GPIO2 - dir_pin: GPIO3 - sleep_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/adc128s102/test.esp32-c3-idf.yaml b/tests/components/adc128s102/test.esp32-c3-idf.yaml deleted file mode 100644 index a60568a736c..00000000000 --- a/tests/components/adc128s102/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO2 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ade7880/test.esp32-c3-idf.yaml b/tests/components/ade7880/test.esp32-c3-idf.yaml deleted file mode 100644 index 7d5b41fc5ae..00000000000 --- a/tests/components/ade7880/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - irq0_pin: GPIO6 - irq1_pin: GPIO7 - reset_pin: GPIO9 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml b/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 59296a1e6ea..00000000000 --- a/tests/components/ade7953_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ade7953_spi/test.esp32-c3-idf.yaml b/tests/components/ade7953_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 5e7e2dc82c0..00000000000 --- a/tests/components/ade7953_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO9 - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ads1115/test.nrf52-adafruit.yaml b/tests/components/ads1115/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/ads1115/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ads1115/test.nrf52-mcumgr.yaml b/tests/components/ads1115/test.nrf52-mcumgr.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/ads1115/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ags10/test.esp32-c3-idf.yaml b/tests/components/ags10/test.esp32-c3-idf.yaml deleted file mode 100644 index 72703301a10..00000000000 --- a/tests/components/ags10/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/aht10/test.esp32-c3-idf.yaml b/tests/components/aht10/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/aht10/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/aht10/test.nrf52-adafruit.yaml b/tests/components/aht10/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/aht10/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/aic3204/test.esp32-c3-idf.yaml b/tests/components/aic3204/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/aic3204/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml b/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/airthings_wave_mini/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml b/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/airthings_wave_plus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/alpha3/test.esp32-c3-idf.yaml b/tests/components/alpha3/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/alpha3/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/am2315c/test.esp32-c3-idf.yaml b/tests/components/am2315c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/am2315c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/am2320/test.esp32-c3-idf.yaml b/tests/components/am2320/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/am2320/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/am43/test.esp32-c3-idf.yaml b/tests/components/am43/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/am43/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/animation/test.esp32-c3-idf.yaml b/tests/components/animation/test.esp32-c3-idf.yaml deleted file mode 100644 index a08a6833337..00000000000 --- a/tests/components/animation/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,13 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - animation: !include common.yaml - -display: - - platform: ili9xxx - id: main_lcd - spi_id: spi_bus - model: ili9342 - cs_pin: 8 - dc_pin: 9 - reset_pin: 10 - invert_colors: false diff --git a/tests/components/anova/test.esp32-c3-idf.yaml b/tests/components/anova/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/anova/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/apds9306/test.esp32-c3-idf.yaml b/tests/components/apds9306/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/apds9306/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/apds9960/test.esp32-c3-idf.yaml b/tests/components/apds9960/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/apds9960/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index c90fa4dfefc..fc53b8ac7e0 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -178,6 +178,14 @@ api: - logger.log: "Skipped loops" - logger.log: "After combined test" +event: + - platform: template + name: Test Event + id: test_event + event_types: + - single_click + - double_click + globals: - id: api_continuation_test_counter type: int diff --git a/tests/components/api/test.esp32-c3-idf.yaml b/tests/components/api/test.esp32-c3-idf.yaml deleted file mode 100644 index 46c01d926f2..00000000000 --- a/tests/components/api/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -<<: !include common.yaml - -wifi: - ssid: MySSID - password: password1 diff --git a/tests/components/as3935_i2c/test.esp32-c3-idf.yaml b/tests/components/as3935_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 59296a1e6ea..00000000000 --- a/tests/components/as3935_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/as3935_spi/test.esp32-c3-idf.yaml b/tests/components/as3935_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 5e7e2dc82c0..00000000000 --- a/tests/components/as3935_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO9 - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/as5600/test.esp32-c3-idf.yaml b/tests/components/as5600/test.esp32-c3-idf.yaml deleted file mode 100644 index 03a87ed6c48..00000000000 --- a/tests/components/as5600/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - dir_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/as7341/test.esp32-c3-idf.yaml b/tests/components/as7341/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/as7341/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/at581x/test.esp32-c3-idf.yaml b/tests/components/at581x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/at581x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml b/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/atc_mithermometer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/atm90e26/test.esp32-c3-idf.yaml b/tests/components/atm90e26/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/atm90e26/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/atm90e32/test.esp32-c3-idf.yaml b/tests/components/atm90e32/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/atm90e32/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/axs15231/test.esp32-c3-idf.yaml b/tests/components/axs15231/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/axs15231/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/b_parasite/test.esp32-c3-idf.yaml b/tests/components/b_parasite/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/b_parasite/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bang_bang/test.esp32-c3-idf.yaml b/tests/components/bang_bang/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/bang_bang/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/bedjet/test.esp32-c3-idf.yaml b/tests/components/bedjet/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/bedjet/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bh1750/test.esp32-c3-idf.yaml b/tests/components/bh1750/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bh1750/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bh1900nux/test.esp32-c3-idf.yaml b/tests/components/bh1900nux/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bh1900nux/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-c3-idf.yaml b/tests/components/binary_sensor/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/binary_sensor/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/binary_sensor/test.esp32-s3-idf.yaml b/tests/components/binary_sensor/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/binary_sensor/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml b/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/binary_sensor_map/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/bl0906/test.esp32-c3-idf.yaml b/tests/components/bl0906/test.esp32-c3-idf.yaml deleted file mode 100644 index 147d967dd43..00000000000 --- a/tests/components/bl0906/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_19200: !include ../../test_build_components/common/uart_19200/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bl0939/test.esp32-c3-idf.yaml b/tests/components/bl0939/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/bl0939/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bl0940/common.yaml b/tests/components/bl0940/common.yaml index 0b73fd6d552..e476ba10c0d 100644 --- a/tests/components/bl0940/common.yaml +++ b/tests/components/bl0940/common.yaml @@ -1,11 +1,11 @@ button: - platform: bl0940 - bl0940_id: test_id + bl0940_id: bl0940_test_id name: Cal Reset sensor: - platform: bl0940 - id: test_id + id: bl0940_test_id voltage: name: BL0940 Voltage current: @@ -22,7 +22,7 @@ sensor: number: - platform: bl0940 id: bl0940_number_id - bl0940_id: test_id + bl0940_id: bl0940_test_id current_calibration: name: Cal Current min_value: -5 diff --git a/tests/components/bl0940/test.esp32-c3-idf.yaml b/tests/components/bl0940/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/bl0940/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bl0942/test.esp32-c3-idf.yaml b/tests/components/bl0942/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/bl0942/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ble_client/common.yaml b/tests/components/ble_client/common.yaml index aa4b6394635..4ea1dd60f38 100644 --- a/tests/components/ble_client/common.yaml +++ b/tests/components/ble_client/common.yaml @@ -52,3 +52,25 @@ sensor: name: "BLE Sensor without Lambda" service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" characteristic_uuid: "abcd1237-abcd-1234-abcd-abcd12345678" + +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + +button: + # Test ble_write with lambda that references a component (function pointer) + - platform: template + name: "BLE Write Lambda Test" + on_press: + - ble_client.ble_write: + id: test_blec + service_uuid: "abcd1234-abcd-1234-abcd-abcd12345678" + characteristic_uuid: "abcd1235-abcd-1234-abcd-abcd12345678" + value: !lambda |- + uint8_t val = (uint8_t)id(test_number).state; + return std::vector{0xAA, val, 0xBB}; diff --git a/tests/components/ble_client/test.esp32-c3-idf.yaml b/tests/components/ble_client/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ble_client/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ble_presence/test.esp32-c3-idf.yaml b/tests/components/ble_presence/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ble_presence/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ble_rssi/test.esp32-c3-idf.yaml b/tests/components/ble_rssi/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ble_rssi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ble_scanner/test.esp32-c3-idf.yaml b/tests/components/ble_scanner/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ble_scanner/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bme280_i2c/test.esp32-c3-idf.yaml b/tests/components/bme280_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bme280_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bme280_spi/test.esp32-c3-idf.yaml b/tests/components/bme280_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/bme280_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bme680/test.esp32-c3-idf.yaml b/tests/components/bme680/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bme680/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml b/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/bme68x_bsec2_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmi160/test.esp32-c3-idf.yaml b/tests/components/bmi160/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmi160/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp085/test.esp32-c3-idf.yaml b/tests/components/bmp085/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmp085/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml b/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmp280_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp280_spi/test.esp32-c3-idf.yaml b/tests/components/bmp280_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/bmp280_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml b/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmp3xx_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml b/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/bmp3xx_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bmp581/test.esp32-c3-idf.yaml b/tests/components/bmp581/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/bmp581/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/bp1658cj/test.esp32-c3-idf.yaml b/tests/components/bp1658cj/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/bp1658cj/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/bp5758d/test.esp32-c3-idf.yaml b/tests/components/bp5758d/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/bp5758d/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/button/test.esp32-c3-idf.yaml b/tests/components/button/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/button/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/bytebuffer/test.esp32-c3-idf.yaml b/tests/components/bytebuffer/test.esp32-c3-idf.yaml deleted file mode 100644 index 380ca876286..00000000000 --- a/tests/components/bytebuffer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -!include common.yaml diff --git a/tests/components/canbus/common.yaml b/tests/components/canbus/common.yaml index fd146cc3a30..8bddeb74094 100644 --- a/tests/components/canbus/common.yaml +++ b/tests/components/canbus/common.yaml @@ -37,6 +37,15 @@ canbus: break; } +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Canbus Actions @@ -44,3 +53,7 @@ button: - canbus.send: "abc" - canbus.send: [0, 1, 2] - canbus.send: !lambda return {0, 1, 2}; + # Test canbus.send with lambda that references a component (function pointer) + - canbus.send: !lambda |- + uint8_t val = (uint8_t)id(test_number).state; + return std::vector{0xAA, val, 0xBB}; diff --git a/tests/components/cap1188/test.esp32-c3-idf.yaml b/tests/components/cap1188/test.esp32-c3-idf.yaml deleted file mode 100644 index c97f30d52c1..00000000000 --- a/tests/components/cap1188/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/captive_portal/test.esp32-c3-idf.yaml b/tests/components/captive_portal/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/captive_portal/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ccs811/test.esp32-c3-idf.yaml b/tests/components/ccs811/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ccs811/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cd74hc4067/test.esp32-c3-idf.yaml b/tests/components/cd74hc4067/test.esp32-c3-idf.yaml deleted file mode 100644 index 5e8784c1fc5..00000000000 --- a/tests/components/cd74hc4067/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - pin_s0: GPIO2 - pin_s1: GPIO3 - pin_s2: GPIO4 - pin_s3: GPIO5 - pin: GPIO0 - -<<: !include common.yaml diff --git a/tests/components/ch422g/test.esp32-c3-idf.yaml b/tests/components/ch422g/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ch422g/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/chsc6x/test.esp32-c3-idf.yaml b/tests/components/chsc6x/test.esp32-c3-idf.yaml deleted file mode 100644 index f0de4107d79..00000000000 --- a/tests/components/chsc6x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,20 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -display: - - platform: ili9xxx - id: ili9xxx_display - model: GC9A01A - invert_colors: True - cs_pin: 11 - dc_pin: 7 - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - -touchscreen: - - platform: chsc6x - display: ili9xxx_display - interrupt_pin: 20 diff --git a/tests/components/chsc6x/test.rp2040-ard.yaml b/tests/components/chsc6x/test.rp2040-ard.yaml index 89cc1b74772..2e3613a4a36 100644 --- a/tests/components/chsc6x/test.rp2040-ard.yaml +++ b/tests/components/chsc6x/test.rp2040-ard.yaml @@ -16,5 +16,6 @@ display: touchscreen: - platform: chsc6x + i2c_id: i2c_bus display: ili9xxx_display interrupt_pin: 22 diff --git a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml b/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cm1106/test.esp32-c3-idf.yaml b/tests/components/cm1106/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/cm1106/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/color/test.esp32-c3-idf.yaml b/tests/components/color/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/color/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/color_temperature/test.esp32-c3-idf.yaml b/tests/components/color_temperature/test.esp32-c3-idf.yaml deleted file mode 100644 index 016f315d9f1..00000000000 --- a/tests/components/color_temperature/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO6 - pin_o2: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/combination/test.esp32-c3-idf.yaml b/tests/components/combination/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/combination/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/coolix/test.esp32-c3-idf.yaml b/tests/components/coolix/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/coolix/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/copy/test.esp32-c3-idf.yaml b/tests/components/copy/test.esp32-c3-idf.yaml deleted file mode 100644 index 76272beb776..00000000000 --- a/tests/components/copy/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - pwm_platform: ledc - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/cs5460a/test.esp32-c3-idf.yaml b/tests/components/cs5460a/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/cs5460a/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cse7761/test.esp32-c3-idf.yaml b/tests/components/cse7761/test.esp32-c3-idf.yaml deleted file mode 100644 index 4e11c6e7cbb..00000000000 --- a/tests/components/cse7761/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart_38400: !include ../../test_build_components/common/uart_38400/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cse7766/test.esp32-c3-idf.yaml b/tests/components/cse7766/test.esp32-c3-idf.yaml deleted file mode 100644 index dc95c985c7a..00000000000 --- a/tests/components/cse7766/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart_4800_even: !include ../../test_build_components/common/uart_4800_even/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cst226/test.esp32-c3-idf.yaml b/tests/components/cst226/test.esp32-c3-idf.yaml deleted file mode 100644 index ffc12867d0f..00000000000 --- a/tests/components/cst226/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - cs_pin: GPIO7 - dc_pin: GPIO9 - disp_reset_pin: GPIO18 - interrupt_pin: GPIO2 - reset_pin: GPIO3 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cst816/test.esp32-c3-idf.yaml b/tests/components/cst816/test.esp32-c3-idf.yaml deleted file mode 100644 index ffc12867d0f..00000000000 --- a/tests/components/cst816/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - cs_pin: GPIO7 - dc_pin: GPIO9 - disp_reset_pin: GPIO18 - interrupt_pin: GPIO2 - reset_pin: GPIO3 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ct_clamp/test.esp32-c3-idf.yaml b/tests/components/ct_clamp/test.esp32-c3-idf.yaml deleted file mode 100644 index a8f29c98ae8..00000000000 --- a/tests/components/ct_clamp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO0 - -<<: !include common.yaml diff --git a/tests/components/current_based/test.esp32-c3-idf.yaml b/tests/components/current_based/test.esp32-c3-idf.yaml deleted file mode 100644 index 59296a1e6ea..00000000000 --- a/tests/components/current_based/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - irq_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/cwww/test.esp32-c3-idf.yaml b/tests/components/cwww/test.esp32-c3-idf.yaml deleted file mode 100644 index 51571b34cfe..00000000000 --- a/tests/components/cwww/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,17 +0,0 @@ -substitutions: - light_platform: ledc - pin_o1: GPIO6 - pin_o2: GPIO7 - -output: - - platform: ${light_platform} - id: light_output_1 - pin: ${pin_o1} - channel: 0 - - platform: ${light_platform} - id: light_output_2 - pin: ${pin_o2} - channel: 1 - phase_angle: 180° - -<<: !include common.yaml diff --git a/tests/components/dac7678/test.esp32-c3-idf.yaml b/tests/components/dac7678/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/dac7678/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/daikin_brc/test.esp32-c3-idf.yaml b/tests/components/daikin_brc/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/daikin_brc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dallas_temp/common.yaml b/tests/components/dallas_temp/common.yaml index 6d865d8e930..abd8e0cfa36 100644 --- a/tests/components/dallas_temp/common.yaml +++ b/tests/components/dallas_temp/common.yaml @@ -9,3 +9,6 @@ sensor: resolution: 9 - platform: dallas_temp name: Dallas Temperature 2 + - platform: dallas_temp + name: Dallas Temperature 3 + index: 2 diff --git a/tests/components/dallas_temp/test.esp32-c3-idf.yaml b/tests/components/dallas_temp/test.esp32-c3-idf.yaml deleted file mode 100644 index 49bf988eb41..00000000000 --- a/tests/components/dallas_temp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - one_wire_pin: "4" - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/daly_bms/test.esp32-c3-idf.yaml b/tests/components/daly_bms/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/daly_bms/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-c3-idf.yaml b/tests/components/debug/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/debug/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/absolute_humidity/test.esp32-c3-idf.yaml b/tests/components/debug/test.nrf52-xiao-ble.yaml similarity index 100% rename from tests/components/absolute_humidity/test.esp32-c3-idf.yaml rename to tests/components/debug/test.nrf52-xiao-ble.yaml diff --git a/tests/components/delonghi/test.esp32-c3-idf.yaml b/tests/components/delonghi/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/delonghi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dfplayer/test.esp32-c3-idf.yaml b/tests/components/dfplayer/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/dfplayer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml b/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/dfrobot_sen0395/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dht/test.esp32-c3-idf.yaml b/tests/components/dht/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/dht/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/dht12/test.esp32-c3-idf.yaml b/tests/components/dht12/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/dht12/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/dps310/test.esp32-c3-idf.yaml b/tests/components/dps310/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/dps310/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ds1307/test.esp32-c3-idf.yaml b/tests/components/ds1307/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ds1307/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ds1307/test.nrf52-adafruit.yaml b/tests/components/ds1307/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/ds1307/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ds2484/test.esp32-c3-idf.yaml b/tests/components/ds2484/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ds2484/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.esp32-c3-idf.yaml b/tests/components/duty_cycle/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/duty_cycle/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/duty_time/test.esp32-c3-idf.yaml b/tests/components/duty_time/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/duty_time/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/e131/test.esp32-c3-idf.yaml b/tests/components/e131/test.esp32-c3-idf.yaml deleted file mode 100644 index d9dc4f68048..00000000000 --- a/tests/components/e131/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: esp32_rmt_led_strip - pin: GPIO2 - -<<: !include common-idf.yaml diff --git a/tests/components/ee895/test.esp32-c3-idf.yaml b/tests/components/ee895/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ee895/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ektf2232/test.esp32-c3-idf.yaml b/tests/components/ektf2232/test.esp32-c3-idf.yaml deleted file mode 100644 index 708d352a595..00000000000 --- a/tests/components/ektf2232/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - display_reset_pin: GPIO3 - interrupt_pin: GPIO6 - touch_reset_pin: GPIO7 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/emc2101/test.esp32-c3-idf.yaml b/tests/components/emc2101/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/emc2101/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/endstop/test.esp32-c3-idf.yaml b/tests/components/endstop/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/endstop/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ens160_i2c/test.esp32-c3-idf.yaml b/tests/components/ens160_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ens160_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ens160_spi/test.esp32-c3-idf.yaml b/tests/components/ens160_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/ens160_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ens210/test.esp32-c3-idf.yaml b/tests/components/ens210/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ens210/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 34aefb82b44..cff1f518978 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -4,7 +4,10 @@ packages: display: - platform: epaper_spi spi_id: spi_bus - model: 7.3in-spectra-e6 + model: spectra-e6 + dimensions: + width: 800 + height: 480 cs_pin: GPIO5 dc_pin: GPIO17 reset_pin: GPIO16 @@ -13,3 +16,6 @@ display: update_interval: 60s lambda: |- it.circle(64, 64, 50, Color::BLACK); + + - platform: epaper_spi + model: seeed-reterminal-e1002 diff --git a/tests/components/es7210/test.esp32-c3-idf.yaml b/tests/components/es7210/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es7210/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/es7243e/test.esp32-c3-idf.yaml b/tests/components/es7243e/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es7243e/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/es8156/test.esp32-c3-idf.yaml b/tests/components/es8156/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es8156/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-c3-idf.yaml b/tests/components/es8311/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es8311/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/es8388/test.esp32-c3-idf.yaml b/tests/components/es8388/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/es8388/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/esp32/common.yaml b/tests/components/esp32/common.yaml new file mode 100644 index 00000000000..039a2610160 --- /dev/null +++ b/tests/components/esp32/common.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/esp32/test-stack_size.esp32-idf.yaml b/tests/components/esp32/test-stack_size.esp32-idf.yaml new file mode 100644 index 00000000000..49535880352 --- /dev/null +++ b/tests/components/esp32/test-stack_size.esp32-idf.yaml @@ -0,0 +1,6 @@ +esp32: + board: esp32dev + framework: + type: esp-idf + advanced: + loop_task_stack_size: 16384 diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml new file mode 100644 index 00000000000..a4c930f2363 --- /dev/null +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -0,0 +1,27 @@ +esp32: + variant: esp32p4 + flash_size: 32MB + cpu_frequency: 400MHz + framework: + type: esp-idf + advanced: + enable_idf_experimental_features: yes + +ota: + platform: esphome + +wifi: + ssid: MySSID + password: password1 + +esp32_hosted: + variant: ESP32C6 + slot: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + d1_pin: GPIO10 + d2_pin: GPIO9 + d3_pin: GPIO8 diff --git a/tests/components/esp32_hosted/.gitattributes b/tests/components/esp32_hosted/.gitattributes new file mode 100644 index 00000000000..6abdc561178 --- /dev/null +++ b/tests/components/esp32_hosted/.gitattributes @@ -0,0 +1 @@ +*.bin -text diff --git a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml index dade44d145b..2a76f17e2fb 100644 --- a/tests/components/esp32_hosted/test.esp32-p4-idf.yaml +++ b/tests/components/esp32_hosted/test.esp32-p4-idf.yaml @@ -1 +1,7 @@ <<: !include common.yaml + +update: + - platform: esp32_hosted + name: "Coprocessor Firmware Update" + path: $component_dir/test_firmware.bin + sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31 diff --git a/tests/components/esp32_hosted/test_firmware.bin b/tests/components/esp32_hosted/test_firmware.bin new file mode 100644 index 00000000000..c97c12f9b0a Binary files /dev/null and b/tests/components/esp32_hosted/test_firmware.bin differ diff --git a/tests/components/esp8266/test.esp8266-ard.yaml b/tests/components/esp8266/test.esp8266-ard.yaml new file mode 100644 index 00000000000..039a2610160 --- /dev/null +++ b/tests/components/esp8266/test.esp8266-ard.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/esphome/test.esp32-c3-idf.yaml b/tests/components/esphome/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/esphome/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/event/test.esp32-c3-idf.yaml b/tests/components/event/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/event/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/exposure_notifications/test.esp32-c3-idf.yaml b/tests/components/exposure_notifications/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/exposure_notifications/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/external_components/test.esp32-c3-idf.yaml b/tests/components/external_components/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/external_components/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/ezo/test.esp32-c3-idf.yaml b/tests/components/ezo/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ezo/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ezo_pmp/test.esp32-c3-idf.yaml b/tests/components/ezo_pmp/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ezo_pmp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/factory_reset/test.esp32-c3-idf.yaml b/tests/components/factory_reset/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/factory_reset/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/feedback/test.esp32-c3-idf.yaml b/tests/components/feedback/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/feedback/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml b/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml deleted file mode 100644 index dff4f7fd924..00000000000 --- a/tests/components/fingerprint_grow/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sensing_pin: GPIO6 -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/font/common.yaml b/tests/components/font/common.yaml index 6ba52e3d975..c156b4aea19 100644 --- a/tests/components/font/common.yaml +++ b/tests/components/font/common.yaml @@ -21,12 +21,12 @@ font: id: roboto_greek size: 20 glyphs: ["\u0300", "\u00C5", "\U000000C7"] - - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + - file: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft size: 20 - file: type: web - url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + url: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft2 size: 24 - file: $component_dir/Monocraft.ttf diff --git a/tests/components/font/test.esp32-c3-idf.yaml b/tests/components/font/test.esp32-c3-idf.yaml deleted file mode 100644 index 2090db7e6d1..00000000000 --- a/tests/components/font/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - display_reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/font/test.host.yaml b/tests/components/font/test.host.yaml index c5399f28260..387ea47335d 100644 --- a/tests/components/font/test.host.yaml +++ b/tests/components/font/test.host.yaml @@ -21,12 +21,12 @@ font: id: roboto_greek size: 20 glyphs: ["\u0300", "\u00C5", "\U000000C7"] - - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + - file: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft size: 20 - file: type: web - url: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" + url: "https://media.esphome.io/tests/fonts/Monocraft.ttf" id: monocraft2 size: 24 - file: $component_dir/Monocraft.ttf diff --git a/tests/components/fs3000/test.esp32-c3-idf.yaml b/tests/components/fs3000/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/fs3000/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ft5x06/test.esp32-c3-idf.yaml b/tests/components/ft5x06/test.esp32-c3-idf.yaml deleted file mode 100644 index c97f30d52c1..00000000000 --- a/tests/components/ft5x06/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ft63x6/test.esp32-c3-idf.yaml b/tests/components/ft63x6/test.esp32-c3-idf.yaml deleted file mode 100644 index febf38d8e10..00000000000 --- a/tests/components/ft63x6/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/fujitsu_general/test.esp32-c3-idf.yaml b/tests/components/fujitsu_general/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/fujitsu_general/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gcja5/test.esp32-c3-idf.yaml b/tests/components/gcja5/test.esp32-c3-idf.yaml deleted file mode 100644 index 236529042fd..00000000000 --- a/tests/components/gcja5/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gdk101/common.yaml b/tests/components/gdk101/common.yaml index 4eb5586ade5..b9b93d760f3 100644 --- a/tests/components/gdk101/common.yaml +++ b/tests/components/gdk101/common.yaml @@ -11,8 +11,6 @@ sensor: name: Radiation Dose @ 10 min status: name: Status - version: - name: FW Version measurement_duration: name: Measuring Time @@ -21,3 +19,8 @@ binary_sensor: gdk101_id: my_gdk101 vibrations: name: Vibrations + +text_sensor: + - platform: gdk101 + version: + name: FW Version diff --git a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml b/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/gl_r01_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/globals/test.esp32-c3-idf.yaml b/tests/components/globals/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/globals/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml b/tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml deleted file mode 100644 index 0e331c893c5..00000000000 --- a/tests/components/gp2y1010au0f/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - adc_pin: GPIO0 - output_pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/gp8403/common.yaml b/tests/components/gp8403/common.yaml index b04fbc4fbca..1147316b026 100644 --- a/tests/components/gp8403/common.yaml +++ b/tests/components/gp8403/common.yaml @@ -4,6 +4,11 @@ gp8403: voltage: 5V - id: gp8403_10v i2c_id: i2c_bus + model: GP8403 + voltage: 10V + - id: gp8413 + i2c_id: i2c_bus + model: GP8413 voltage: 10V output: @@ -15,3 +20,7 @@ output: gp8403_id: gp8403_10v id: gp8403_output_1 channel: 1 + - platform: gp8403 + gp8403_id: gp8413 + id: gp8413_output_2 + channel: 1 diff --git a/tests/components/gp8403/test.esp32-c3-idf.yaml b/tests/components/gp8403/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/gp8403/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gps/test.esp32-c3-idf.yaml b/tests/components/gps/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/gps/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/graph/test.esp32-c3-idf.yaml b/tests/components/graph/test.esp32-c3-idf.yaml deleted file mode 100644 index c97f30d52c1..00000000000 --- a/tests/components/graph/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml b/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml deleted file mode 100644 index c97f30d52c1..00000000000 --- a/tests/components/graphical_display_menu/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gree/test.esp32-c3-idf.yaml b/tests/components/gree/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/gree/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml b/tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/grove_gas_mc_v2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml b/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/grove_tb6612fng/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/growatt_solar/test.esp32-c3-idf.yaml b/tests/components/growatt_solar/test.esp32-c3-idf.yaml deleted file mode 100644 index 17940aafcf2..00000000000 --- a/tests/components/growatt_solar/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - flow_control_pin: GPIO3 -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/gt911/test.esp32-c3-idf.yaml b/tests/components/gt911/test.esp32-c3-idf.yaml deleted file mode 100644 index 5e15963b7e6..00000000000 --- a/tests/components/gt911/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - display_reset_pin: "18" - interrupt_pin: "20" - reset_pin: "21" - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/haier/test.esp32-c3-idf.yaml b/tests/components/haier/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/haier/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/havells_solar/test.esp32-c3-idf.yaml b/tests/components/havells_solar/test.esp32-c3-idf.yaml deleted file mode 100644 index 17940aafcf2..00000000000 --- a/tests/components/havells_solar/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - flow_control_pin: GPIO3 -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hbridge/test.esp32-c3-idf.yaml b/tests/components/hbridge/test.esp32-c3-idf.yaml deleted file mode 100644 index 93a6cb58184..00000000000 --- a/tests/components/hbridge/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,17 +0,0 @@ -substitutions: - pwm_platform: "ledc" - output1_pin: "4" - output2_pin: "5" - output3_pin: "6" - output4_pin: "7" - hbridge_on_pin: "2" - hbridge_off_pin: "3" - -<<: !include common.yaml - -switch: - - platform: hbridge - id: switch_hbridge - on_pin: ${hbridge_on_pin} - off_pin: ${hbridge_off_pin} - pulse_length: 60ms diff --git a/tests/components/hdc1080/test.esp32-c3-idf.yaml b/tests/components/hdc1080/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hdc1080/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hdc2010/test.esp32-c3-idf.yaml b/tests/components/hdc2010/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hdc2010/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/he60r/test.esp32-c3-idf.yaml b/tests/components/he60r/test.esp32-c3-idf.yaml deleted file mode 100644 index b0c8c5de3d5..00000000000 --- a/tests/components/he60r/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_1200_even: !include ../../test_build_components/common/uart_1200_even/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hlk_fm22x/common.yaml b/tests/components/hlk_fm22x/common.yaml new file mode 100644 index 00000000000..6fcd9af5949 --- /dev/null +++ b/tests/components/hlk_fm22x/common.yaml @@ -0,0 +1,41 @@ +esphome: + on_boot: + then: + - hlk_fm22x.enroll: + name: "Test" + direction: 1 + - hlk_fm22x.delete_all: + +hlk_fm22x: + on_face_scan_matched: + - logger.log: test_hlk_22x_face_scan_matched + on_face_scan_unmatched: + - logger.log: test_hlk_22x_face_scan_unmatched + on_face_scan_invalid: + - logger.log: test_hlk_22x_face_scan_invalid + on_face_info: + - logger.log: test_hlk_22x_face_info + on_enrollment_done: + - logger.log: test_hlk_22x_enrollment_done + on_enrollment_failed: + - logger.log: test_hlk_22x_enrollment_failed + +sensor: + - platform: hlk_fm22x + face_count: + name: "Face Count" + last_face_id: + name: "Last Face ID" + status: + name: "Face Status" + +binary_sensor: + - platform: hlk_fm22x + name: "Face Enrolling" + +text_sensor: + - platform: hlk_fm22x + version: + name: "HLK Version" + last_face_name: + name: "Last Face Name" diff --git a/tests/components/hlk_fm22x/test.esp32-idf.yaml b/tests/components/hlk_fm22x/test.esp32-idf.yaml new file mode 100644 index 00000000000..2d29656c94a --- /dev/null +++ b/tests/components/hlk_fm22x/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/hlk_fm22x/test.esp8266-ard.yaml b/tests/components/hlk_fm22x/test.esp8266-ard.yaml new file mode 100644 index 00000000000..5a05efa259b --- /dev/null +++ b/tests/components/hlk_fm22x/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hlk_fm22x/test.rp2040-ard.yaml b/tests/components/hlk_fm22x/test.rp2040-ard.yaml new file mode 100644 index 00000000000..f1df2daf83a --- /dev/null +++ b/tests/components/hlk_fm22x/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/hlw8012/test.esp32-c3-idf.yaml b/tests/components/hlw8012/test.esp32-c3-idf.yaml deleted file mode 100644 index 8b0d069ce2e..00000000000 --- a/tests/components/hlw8012/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO2 - cf_pin: GPIO3 - cf1_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/hm3301/test.esp32-c3-idf.yaml b/tests/components/hm3301/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hm3301/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hmc5883l/test.esp32-c3-idf.yaml b/tests/components/hmc5883l/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hmc5883l/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/homeassistant/test.esp32-c3-idf.yaml b/tests/components/homeassistant/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/homeassistant/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml b/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/honeywell_hih_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/honeywellabp/test.esp32-c3-idf.yaml b/tests/components/honeywellabp/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/honeywellabp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml b/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/honeywellabp2_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/host/common.yaml b/tests/components/host/common.yaml index 5c329c8245c..d5c8446ae8a 100644 --- a/tests/components/host/common.yaml +++ b/tests/components/host/common.yaml @@ -15,3 +15,10 @@ esphome: static const uint8_t my_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; if (!mac_address_is_valid(my_addr)) ESP_LOGD("test", "Invalid mac address %X", my_addr[0]); // etc. + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml b/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/hrxl_maxsonar_wr/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hte501/test.esp32-c3-idf.yaml b/tests/components/hte501/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hte501/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/http_request/test.esp32-c3-idf.yaml b/tests/components/http_request/test.esp32-c3-idf.yaml deleted file mode 100644 index ee2f5aa59b8..00000000000 --- a/tests/components/http_request/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - verify_ssl: "true" - -<<: !include common.yaml diff --git a/tests/components/htu21d/test.esp32-c3-idf.yaml b/tests/components/htu21d/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/htu21d/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/htu31d/test.esp32-c3-idf.yaml b/tests/components/htu31d/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/htu31d/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hx711/test.esp32-c3-idf.yaml b/tests/components/hx711/test.esp32-c3-idf.yaml deleted file mode 100644 index defef165e31..00000000000 --- a/tests/components/hx711/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO4 - dout_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml b/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/hydreon_rgxx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/hyt271/test.esp32-c3-idf.yaml b/tests/components/hyt271/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/hyt271/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/i2c/test.esp32-c3-idf.yaml b/tests/components/i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-adafruit.yaml b/tests/components/i2c/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/i2c/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-mcumgr.yaml b/tests/components/i2c/test.nrf52-mcumgr.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/i2c/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c/test.nrf52-xiao-ble.yaml b/tests/components/i2c/test.nrf52-xiao-ble.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/i2c/test.nrf52-xiao-ble.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/i2c_device/test.esp32-c3-idf.yaml b/tests/components/i2c_device/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/i2c_device/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/iaqcore/test.esp32-c3-idf.yaml b/tests/components/iaqcore/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/iaqcore/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ili9xxx/test.esp32-c3-idf.yaml b/tests/components/ili9xxx/test.esp32-c3-idf.yaml deleted file mode 100644 index 1eea1e85f76..00000000000 --- a/tests/components/ili9xxx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - cs_pin1: GPIO8 - dc_pin1: GPIO9 - reset_pin1: GPIO10 - cs_pin2: GPIO2 - dc_pin2: GPIO3 - reset_pin2: GPIO7 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/image/common.yaml b/tests/components/image/common.yaml index 864ca41c44a..98190689708 100644 --- a/tests/components/image/common.yaml +++ b/tests/components/image/common.yaml @@ -50,16 +50,16 @@ image: transparency: opaque - id: web_svg_image - file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg + file: https://media.esphome.io/logo/logo.svg resize: 256x48 type: BINARY transparency: chroma_key - id: web_tiff_image - file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff + file: https://media.esphome.io/tests/images/SIPI_Jelly_Beans_4.1.07.tiff type: RGB resize: 48x48 - id: web_redirect_image - file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 + file: https://media.esphome.io/logo/logo.png type: RGB resize: 48x48 - id: mdi_alert diff --git a/tests/components/image/test.host.yaml b/tests/components/image/test.host.yaml index 0411195e2a3..aa454970882 100644 --- a/tests/components/image/test.host.yaml +++ b/tests/components/image/test.host.yaml @@ -1,5 +1,6 @@ display: - platform: sdl + id: image_display auto_clear_enabled: false dimensions: width: 480 diff --git a/tests/components/ina219/test.esp32-c3-idf.yaml b/tests/components/ina219/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina219/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina226/test.esp32-c3-idf.yaml b/tests/components/ina226/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina226/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina260/test.esp32-c3-idf.yaml b/tests/components/ina260/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina260/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml b/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina2xx_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml b/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/ina2xx_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ina3221/test.esp32-c3-idf.yaml b/tests/components/ina3221/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ina3221/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml b/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/inkbird_ibsth1_mini/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/inkplate/test.esp32-idf.yaml b/tests/components/inkplate/test.esp32-idf.yaml index b47e39c3898..17e58ce3902 100644 --- a/tests/components/inkplate/test.esp32-idf.yaml +++ b/tests/components/inkplate/test.esp32-idf.yaml @@ -1,4 +1,7 @@ packages: i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml +psram: + mode: quad + <<: !include common.yaml diff --git a/tests/components/integration/test.esp32-c3-idf.yaml b/tests/components/integration/test.esp32-c3-idf.yaml deleted file mode 100644 index 5105e645f3c..00000000000 --- a/tests/components/integration/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common-esp32.yaml diff --git a/tests/components/interval/test.esp32-c3-idf.yaml b/tests/components/interval/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/interval/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml b/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/jsn_sr04t/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/key_collector/test.esp32-c3-idf.yaml b/tests/components/key_collector/test.esp32-c3-idf.yaml deleted file mode 100644 index b580ab78432..00000000000 --- a/tests/components/key_collector/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_r0: GPIO2 - pin_r1: GPIO3 - pin_c0: GPIO4 - pin_c1: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/kmeteriso/test.esp32-c3-idf.yaml b/tests/components/kmeteriso/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/kmeteriso/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/kuntze/test.esp32-c3-idf.yaml b/tests/components/kuntze/test.esp32-c3-idf.yaml deleted file mode 100644 index 17940aafcf2..00000000000 --- a/tests/components/kuntze/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - flow_control_pin: GPIO3 -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/lc709203f/test.esp32-c3-idf.yaml b/tests/components/lc709203f/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/lc709203f/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/lcd_gpio/test.esp32-c3-idf.yaml b/tests/components/lcd_gpio/test.esp32-c3-idf.yaml deleted file mode 100644 index b6b05f3ab4f..00000000000 --- a/tests/components/lcd_gpio/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO1 - d1_pin: GPIO2 - d2_pin: GPIO3 - d3_pin: GPIO4 - enable_pin: GPIO5 - rs_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/lcd_menu/test.esp32-c3-idf.yaml b/tests/components/lcd_menu/test.esp32-c3-idf.yaml deleted file mode 100644 index b6b05f3ab4f..00000000000 --- a/tests/components/lcd_menu/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - d0_pin: GPIO1 - d1_pin: GPIO2 - d2_pin: GPIO3 - d3_pin: GPIO4 - enable_pin: GPIO5 - rs_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml b/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/lcd_pcf8574/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ld2410/test.esp32-c3-idf.yaml b/tests/components/ld2410/test.esp32-c3-idf.yaml deleted file mode 100644 index 7a8f790ed8b..00000000000 --- a/tests/components/ld2410/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ld2412/test.esp32-c3-idf.yaml b/tests/components/ld2412/test.esp32-c3-idf.yaml deleted file mode 100644 index 7a8f790ed8b..00000000000 --- a/tests/components/ld2412/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ld2420/test.esp32-c3-idf.yaml b/tests/components/ld2420/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/ld2420/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ld2450/test.esp32-c3-idf.yaml b/tests/components/ld2450/test.esp32-c3-idf.yaml deleted file mode 100644 index 7a8f790ed8b..00000000000 --- a/tests/components/ld2450/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - tx_pin: GPIO4 - rx_pin: GPIO5 - -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/libretiny/test.bk72xx-ard.yaml b/tests/components/libretiny/test.bk72xx-ard.yaml new file mode 100644 index 00000000000..039a2610160 --- /dev/null +++ b/tests/components/libretiny/test.bk72xx-ard.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/light/test.esp32-c3-idf.yaml b/tests/components/light/test.esp32-c3-idf.yaml deleted file mode 100644 index 317d5748a3d..00000000000 --- a/tests/components/light/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,21 +0,0 @@ -output: - - platform: gpio - id: test_binary - pin: 0 - - platform: ledc - id: test_ledc_1 - pin: 1 - - platform: ledc - id: test_ledc_2 - pin: 2 - - platform: ledc - id: test_ledc_3 - pin: 3 - - platform: ledc - id: test_ledc_4 - pin: 4 - - platform: ledc - id: test_ledc_5 - pin: 5 - -<<: !include common.yaml diff --git a/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml b/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml deleted file mode 100644 index febf38d8e10..00000000000 --- a/tests/components/lilygo_t5_47/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/lm75b/test.esp32-c3-idf.yaml b/tests/components/lm75b/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/lm75b/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/lock/test.esp32-c3-idf.yaml b/tests/components/lock/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/lock/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/lps22/test.esp32-c3-idf.yaml b/tests/components/lps22/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/lps22/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ltr390/test.esp32-c3-idf.yaml b/tests/components/ltr390/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ltr390/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ltr501/test.esp32-c3-idf.yaml b/tests/components/ltr501/test.esp32-c3-idf.yaml deleted file mode 100644 index 72703301a10..00000000000 --- a/tests/components/ltr501/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml b/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml deleted file mode 100644 index 72703301a10..00000000000 --- a/tests/components/ltr_als_ps/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c_low_freq: !include ../../test_build_components/common/i2c_low_freq/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 14241a16693..d7c342b16e7 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -113,9 +113,10 @@ lvgl: title: Messagebox bg_color: 0xffff widgets: - - label: - text: Hello Msgbox - id: msgbox_label + # Test single widget without list + label: + text: Hello Msgbox + id: msgbox_label body: text: This is a sample messagebox bg_color: 0x808080 @@ -190,7 +191,7 @@ lvgl: args: ['lv_event_code_name_for(event->code).c_str()'] skip: true layout: - type: flex + type: Flex pad_row: 4 pad_column: 4px flex_align_main: center @@ -281,7 +282,7 @@ lvgl: #endif return std::string(buf); align: top_left - - obj: + - container: align: center arc_opa: COVER arc_color: 0xFF0000 @@ -414,6 +415,7 @@ lvgl: - buttons: - id: button_e - button: + layout: 2x1 id: button_button width: 20% height: 10% @@ -430,8 +432,13 @@ lvgl: checked: bg_color: 0x000000 widgets: - - label: - text: Button + # Test parse a dict instead of list + label: + text: Button + align: bottom_right + image: + src: cat_image + align: top_left on_click: - lvgl.widget.focus: spin_up - lvgl.widget.focus: next @@ -539,6 +546,7 @@ lvgl: - logger.log: "tile 1 is now showing" tiles: - id: tile_1 + layout: vertical row: 0 column: 0 dir: ALL @@ -554,6 +562,7 @@ lvgl: bg_color: 0x000000 - id: page2 + layout: vertical widgets: - canvas: id: canvas_id @@ -691,6 +700,10 @@ lvgl: width: 100% height: 10% align: top_mid + on_value: + - lvgl.spinbox.update: + id: spinbox_id + value: !lambda return x; - button: styles: spin_button id: spin_up @@ -717,6 +730,12 @@ lvgl: - logger.log: format: "Spinbox value is %f" args: [x] + - lvgl.label.update: + id: hello_label + text: + format: "value is %.1f now" + args: [x] + if_nan: "Value unknown" - button: styles: spin_button id: spin_down @@ -854,7 +873,7 @@ lvgl: width: 100% pad_all: 8 layout: - type: grid + type: GRid grid_row_align: end grid_rows: [25px, fr(1), content] grid_columns: [40, fr(1), fr(1)] @@ -1005,6 +1024,7 @@ lvgl: r_mod: -20 opa: 0% - id: page3 + layout: Horizontal widgets: - keyboard: id: lv_keyboard diff --git a/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml b/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/m5stack_8angle/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mapping/test.esp32-c3-idf.yaml b/tests/components/mapping/test.esp32-c3-idf.yaml deleted file mode 100644 index 7911eb7edc7..00000000000 --- a/tests/components/mapping/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,13 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - map: !include common.yaml - -display: - spi_id: spi_bus - platform: ili9xxx - id: main_lcd - model: ili9342 - cs_pin: 8 - dc_pin: 9 - reset_pin: 10 - invert_colors: false diff --git a/tests/components/matrix_keypad/test.esp32-c3-idf.yaml b/tests/components/matrix_keypad/test.esp32-c3-idf.yaml deleted file mode 100644 index 75d9c0b263a..00000000000 --- a/tests/components/matrix_keypad/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - common: !include common.yaml - -matrix_keypad: - id: keypad - rows: - - pin: 1 - - pin: 2 - columns: - - pin: 3 - - pin: 4 - keys: "1234" - has_pulldowns: true - on_key: - - lambda: ESP_LOGI("KEY", "key %d pressed", x); diff --git a/tests/components/matrix_keypad/test.esp32-s3-idf.yaml b/tests/components/matrix_keypad/test.esp32-s3-idf.yaml deleted file mode 100644 index a491f2ed59c..00000000000 --- a/tests/components/matrix_keypad/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - common: !include common.yaml - -matrix_keypad: - id: keypad - rows: - - pin: 10 - - pin: 11 - columns: - - pin: 12 - - pin: 13 - keys: "1234" - has_pulldowns: true - on_key: - - lambda: ESP_LOGI("KEY", "key %d pressed", x); diff --git a/tests/components/max17043/test.esp32-c3-idf.yaml b/tests/components/max17043/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/max17043/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max31855/test.esp32-c3-idf.yaml b/tests/components/max31855/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max31855/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max31856/test.esp32-c3-idf.yaml b/tests/components/max31856/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max31856/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max31865/test.esp32-c3-idf.yaml b/tests/components/max31865/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max31865/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max44009/test.esp32-c3-idf.yaml b/tests/components/max44009/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/max44009/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max6675/test.esp32-c3-idf.yaml b/tests/components/max6675/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max6675/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max6956/test.esp32-c3-idf.yaml b/tests/components/max6956/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/max6956/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max7219/test.esp32-c3-idf.yaml b/tests/components/max7219/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max7219/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max7219digit/test.esp32-c3-idf.yaml b/tests/components/max7219digit/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/max7219digit/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/max9611/test.esp32-c3-idf.yaml b/tests/components/max9611/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/max9611/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23008/test.esp32-c3-idf.yaml b/tests/components/mcp23008/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp23008/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23016/test.esp32-c3-idf.yaml b/tests/components/mcp23016/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp23016/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23017/test.esp32-c3-idf.yaml b/tests/components/mcp23017/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp23017/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23s08/test.esp32-c3-idf.yaml b/tests/components/mcp23s08/test.esp32-c3-idf.yaml deleted file mode 100644 index b11ec9cdc67..00000000000 --- a/tests/components/mcp23s08/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO8 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp23s17/test.esp32-c3-idf.yaml b/tests/components/mcp23s17/test.esp32-c3-idf.yaml deleted file mode 100644 index b11ec9cdc67..00000000000 --- a/tests/components/mcp23s17/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO8 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp2515/test.esp32-c3-idf.yaml b/tests/components/mcp2515/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/mcp2515/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp3008/test.esp32-c3-idf.yaml b/tests/components/mcp3008/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/mcp3008/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp3204/test.esp32-c3-idf.yaml b/tests/components/mcp3204/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/mcp3204/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp3221/common.yaml b/tests/components/mcp3221/common.yaml new file mode 100644 index 00000000000..cc3eadbf4fd --- /dev/null +++ b/tests/components/mcp3221/common.yaml @@ -0,0 +1,6 @@ +sensor: + - platform: mcp3221 + id: test_id + name: voltage + i2c_id: i2c_bus + reference_voltage: 3.3V diff --git a/tests/components/ads1115/test.esp32-c3-idf.yaml b/tests/components/mcp3221/test.esp32-c3-idf.yaml similarity index 100% rename from tests/components/ads1115/test.esp32-c3-idf.yaml rename to tests/components/mcp3221/test.esp32-c3-idf.yaml diff --git a/tests/components/mcp3221/test.esp32-idf.yaml b/tests/components/mcp3221/test.esp32-idf.yaml new file mode 100644 index 00000000000..b47e39c3898 --- /dev/null +++ b/tests/components/mcp3221/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/mcp3221/test.esp8266-ard.yaml b/tests/components/mcp3221/test.esp8266-ard.yaml new file mode 100644 index 00000000000..4a98b9388ab --- /dev/null +++ b/tests/components/mcp3221/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/mcp3221/test.rp2040-ard.yaml b/tests/components/mcp3221/test.rp2040-ard.yaml new file mode 100644 index 00000000000..319a7c71a65 --- /dev/null +++ b/tests/components/mcp3221/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/mcp4461/test.esp32-c3-idf.yaml b/tests/components/mcp4461/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp4461/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp4725/test.esp32-c3-idf.yaml b/tests/components/mcp4725/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp4725/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp4728/test.esp32-c3-idf.yaml b/tests/components/mcp4728/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp4728/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp47a1/test.esp32-c3-idf.yaml b/tests/components/mcp47a1/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp47a1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp9600/test.esp32-c3-idf.yaml b/tests/components/mcp9600/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp9600/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mcp9808/test.esp32-c3-idf.yaml b/tests/components/mcp9808/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mcp9808/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mdns/test-enabled.esp32-c3-idf.yaml b/tests/components/mdns/test-enabled.esp32-c3-idf.yaml deleted file mode 100644 index 97fd63d70e3..00000000000 --- a/tests/components/mdns/test-enabled.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/mhz19/test.esp32-c3-idf.yaml b/tests/components/mhz19/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/mhz19/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/micronova/test.esp32-c3-idf.yaml b/tests/components/micronova/test.esp32-c3-idf.yaml deleted file mode 100644 index eed876ae746..00000000000 --- a/tests/components/micronova/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - enable_rx_pin: GPIO3 - -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/microphone/test.esp32-c3-idf.yaml b/tests/components/microphone/test.esp32-c3-idf.yaml deleted file mode 100644 index c28dc553f5b..00000000000 --- a/tests/components/microphone/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO6 - i2s_lrclk_pin: GPIO7 - i2s_mclk_pin: GPIO8 - i2s_din_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/mics_4514/test.esp32-c3-idf.yaml b/tests/components/mics_4514/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mics_4514/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp32-c3-idf.yaml b/tests/components/midea_ir/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/midea_ir/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml index 48f34f34499..14f864d3261 100644 --- a/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml +++ b/tests/components/mipi_spi/test-lvgl.esp32-s3-idf.yaml @@ -9,3 +9,4 @@ display: lvgl: psram: + mode: quad diff --git a/tests/components/mitsubishi/test.esp32-c3-idf.yaml b/tests/components/mitsubishi/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/mitsubishi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mixer/test.esp32-c3-idf.yaml b/tests/components/mixer/test.esp32-c3-idf.yaml deleted file mode 100644 index f1721f08625..00000000000 --- a/tests/components/mixer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-c3-idf.yaml b/tests/components/mlx90393/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mlx90393/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mlx90393/test.esp32-s3-idf.yaml b/tests/components/mlx90393/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/mlx90393/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mlx90614/test.esp32-c3-idf.yaml b/tests/components/mlx90614/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mlx90614/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mmc5603/test.esp32-c3-idf.yaml b/tests/components/mmc5603/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mmc5603/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mmc5983/test.esp32-c3-idf.yaml b/tests/components/mmc5983/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mmc5983/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/modbus/test.esp32-c3-idf.yaml b/tests/components/modbus/test.esp32-c3-idf.yaml deleted file mode 100644 index 430c6818cb4..00000000000 --- a/tests/components/modbus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - flow_control_pin: GPIO3 -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/modbus_controller/test.esp32-c3-idf.yaml b/tests/components/modbus_controller/test.esp32-c3-idf.yaml deleted file mode 100644 index db826676ee1..00000000000 --- a/tests/components/modbus_controller/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/monochromatic/test.esp32-c3-idf.yaml b/tests/components/monochromatic/test.esp32-c3-idf.yaml deleted file mode 100644 index feabf013fdf..00000000000 --- a/tests/components/monochromatic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - light_platform: ledc - pin: GPIO2 - -<<: !include common.yaml diff --git a/tests/components/mopeka_ble/test.esp32-c3-idf.yaml b/tests/components/mopeka_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/mopeka_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml b/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/mopeka_pro_check/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml b/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/mopeka_std_check/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mpl3115a2/test.esp32-c3-idf.yaml b/tests/components/mpl3115a2/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mpl3115a2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mpr121/test.esp32-c3-idf.yaml b/tests/components/mpr121/test.esp32-c3-idf.yaml deleted file mode 100644 index d1abb03369b..00000000000 --- a/tests/components/mpr121/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mpu6050/test.esp32-c3-idf.yaml b/tests/components/mpu6050/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mpu6050/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mpu6886/test.esp32-c3-idf.yaml b/tests/components/mpu6886/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/mpu6886/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/mqtt/test.esp32-c3-idf.yaml b/tests/components/mqtt/test.esp32-c3-idf.yaml deleted file mode 100644 index d19609b55e9..00000000000 --- a/tests/components/mqtt/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - common: !include common.yaml - update: !include common-update.yaml diff --git a/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml b/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml deleted file mode 100644 index 2cb6d825367..00000000000 --- a/tests/components/mqtt_subscribe/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-idf.yaml diff --git a/tests/components/ms5611/test.esp32-c3-idf.yaml b/tests/components/ms5611/test.esp32-c3-idf.yaml deleted file mode 100644 index d1abb03369b..00000000000 --- a/tests/components/ms5611/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - i2c_scl: GPIO5 - i2c_sda: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/msa3xx/test.esp32-c3-idf.yaml b/tests/components/msa3xx/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/msa3xx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/my9231/test.esp32-c3-idf.yaml b/tests/components/my9231/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/my9231/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/nau7802/common.yaml b/tests/components/nau7802/common.yaml index 5c52c33dadb..5251910df9f 100644 --- a/tests/components/nau7802/common.yaml +++ b/tests/components/nau7802/common.yaml @@ -1,13 +1,13 @@ sensor: - platform: nau7802 i2c_id: i2c_bus - id: test_id + id: nau7802_test_id name: weight gain: 32 ldo_voltage: "3.0v" samples_per_second: 10 on_value: then: - - nau7802.calibrate_external_offset: test_id - - nau7802.calibrate_internal_offset: test_id - - nau7802.calibrate_gain: test_id + - nau7802.calibrate_external_offset: nau7802_test_id + - nau7802.calibrate_internal_offset: nau7802_test_id + - nau7802.calibrate_gain: nau7802_test_id diff --git a/tests/components/nau7802/test.esp32-c3-idf.yaml b/tests/components/nau7802/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/nau7802/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/network/test-ipv6.esp32-c3-idf.yaml b/tests/components/network/test-ipv6.esp32-c3-idf.yaml deleted file mode 100644 index da1324b17ef..00000000000 --- a/tests/components/network/test-ipv6.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - network_enable_ipv6: "true" - -<<: !include common.yaml diff --git a/tests/components/network/test.esp32-c3-idf.yaml b/tests/components/network/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/network/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/network/test.esp32-idf.yaml b/tests/components/network/test.esp32-idf.yaml index dade44d145b..7c01bafa0df 100644 --- a/tests/components/network/test.esp32-idf.yaml +++ b/tests/components/network/test.esp32-idf.yaml @@ -1 +1,4 @@ <<: !include common.yaml + +network: + enable_high_performance: true diff --git a/tests/components/nextion/test.esp32-c3-idf.yaml b/tests/components/nextion/test.esp32-c3-idf.yaml deleted file mode 100644 index 888693f909a..00000000000 --- a/tests/components/nextion/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - base: !include common.yaml - -display: - - id: !extend main_lcd - tft_url: http://esphome.io/default35.tft diff --git a/tests/components/noblex/test.esp32-c3-idf.yaml b/tests/components/noblex/test.esp32-c3-idf.yaml deleted file mode 100644 index fe77c44eed1..00000000000 --- a/tests/components/noblex/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/npi19/test.esp32-s3-idf.yaml b/tests/components/npi19/test.esp32-s3-idf.yaml deleted file mode 100644 index 0fd8684a2c0..00000000000 --- a/tests/components/npi19/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml index 3fe80209b62..5fa0d6e88fb 100644 --- a/tests/components/nrf52/test.nrf52-adafruit.yaml +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -1,3 +1,13 @@ +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); nrf52: dfu: reset_pin: @@ -5,3 +15,7 @@ nrf52: inverted: true mode: output: true + dcdc: False + reg0: + voltage: 2.1V + uicr_erase: true diff --git a/tests/components/nrf52/test.nrf52-mcumgr.yaml b/tests/components/nrf52/test.nrf52-mcumgr.yaml index e69de29bb2d..89ec637db6a 100644 --- a/tests/components/nrf52/test.nrf52-mcumgr.yaml +++ b/tests/components/nrf52/test.nrf52-mcumgr.yaml @@ -0,0 +1,4 @@ +nrf52: + reg0: + voltage: 3.3V + uicr_erase: true diff --git a/tests/components/nrf52/test.nrf52-xiao-ble.yaml b/tests/components/nrf52/test.nrf52-xiao-ble.yaml index 3fe80209b62..d53c6920017 100644 --- a/tests/components/nrf52/test.nrf52-xiao-ble.yaml +++ b/tests/components/nrf52/test.nrf52-xiao-ble.yaml @@ -5,3 +5,5 @@ nrf52: inverted: true mode: output: true + reg0: + voltage: 1.8V diff --git a/tests/components/ntc/test.esp32-c3-idf.yaml b/tests/components/ntc/test.esp32-c3-idf.yaml deleted file mode 100644 index 37fb325f4a5..00000000000 --- a/tests/components/ntc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ntc/test.esp32-s3-idf.yaml b/tests/components/ntc/test.esp32-s3-idf.yaml deleted file mode 100644 index 37fb325f4a5..00000000000 --- a/tests/components/ntc/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index da5339fb39d..9df63b2f29b 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -2,7 +2,7 @@ network: enable_ipv6: true openthread: - device_type: FTD + device_type: MTD channel: 13 network_name: OpenThread-8f28 network_key: 0xdfd34f0f05cad978ec4e32b0413038ff @@ -11,3 +11,5 @@ openthread: pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 mesh_local_prefix: fd53:145f:ed22:ad81::/64 force_dataset: true + use_address: open-thread-test.local + poll_period: 20sec diff --git a/tests/components/opt3001/test.esp32-c3-idf.yaml b/tests/components/opt3001/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/opt3001/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ota/test.esp32-c3-idf.yaml b/tests/components/ota/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/ota/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/output/test.esp32-c3-idf.yaml b/tests/components/output/test.esp32-c3-idf.yaml deleted file mode 100644 index 22276437034..00000000000 --- a/tests/components/output/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/packet_transport/test.esp32-c3-idf.yaml b/tests/components/packet_transport/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/packet_transport/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pca6416a/test.esp32-c3-idf.yaml b/tests/components/pca6416a/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pca6416a/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pca9554/test.esp32-c3-idf.yaml b/tests/components/pca9554/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pca9554/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pca9685/test.esp32-c3-idf.yaml b/tests/components/pca9685/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pca9685/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pcd8544/test.esp32-c3-idf.yaml b/tests/components/pcd8544/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/pcd8544/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pcf85063/test.esp32-c3-idf.yaml b/tests/components/pcf85063/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pcf85063/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pcf85063/test.nrf52-adafruit.yaml b/tests/components/pcf85063/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/pcf85063/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/pcf8563/test.esp32-c3-idf.yaml b/tests/components/pcf8563/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pcf8563/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pcf8563/test.nrf52-adafruit.yaml b/tests/components/pcf8563/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/pcf8563/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/pcf8574/test.esp32-c3-idf.yaml b/tests/components/pcf8574/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pcf8574/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pid/test.esp32-c3-idf.yaml b/tests/components/pid/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/pid/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pipsolar/test.esp32-c3-idf.yaml b/tests/components/pipsolar/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pipsolar/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pm1006/test.esp32-c3-idf.yaml b/tests/components/pm1006/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pm1006/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pm2005/test.esp32-c3-idf.yaml b/tests/components/pm2005/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pm2005/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pmsa003i/test.esp32-c3-idf.yaml b/tests/components/pmsa003i/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pmsa003i/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pmsx003/test.esp32-c3-idf.yaml b/tests/components/pmsx003/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pmsx003/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pmwcs3/test.esp32-c3-idf.yaml b/tests/components/pmwcs3/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pmwcs3/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn532_i2c/test.esp32-c3-idf.yaml b/tests/components/pn532_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/pn532_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn532_spi/test.esp32-c3-idf.yaml b/tests/components/pn532_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/pn532_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml b/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index cdf84452630..00000000000 --- a/tests/components/pn7150_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - irq_pin: GPIO6 - ven_pin: GPIO7 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml b/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index cdf84452630..00000000000 --- a/tests/components/pn7160_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - irq_pin: GPIO6 - ven_pin: GPIO7 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pn7160_spi/test.esp32-c3-idf.yaml b/tests/components/pn7160_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index ac18bfff5c0..00000000000 --- a/tests/components/pn7160_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - irq_pin: GPIO9 - ven_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/power_supply/test.esp32-c3-idf.yaml b/tests/components/power_supply/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/power_supply/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-c3-idf.yaml b/tests/components/prometheus/test.esp32-c3-idf.yaml deleted file mode 100644 index fedeaf822a9..00000000000 --- a/tests/components/prometheus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - verify_ssl: "false" - -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s3-idf.yaml b/tests/components/psram/test.esp32-s3-idf.yaml index 75d4ee539c1..548b8324d0c 100644 --- a/tests/components/psram/test.esp32-s3-idf.yaml +++ b/tests/components/psram/test.esp32-s3-idf.yaml @@ -9,3 +9,4 @@ psram: mode: octal speed: 120MHz enable_ecc: true + ignore_not_found: false diff --git a/tests/components/pulse_meter/test.esp32-c3-idf.yaml b/tests/components/pulse_meter/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/pulse_meter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pulse_width/test.esp32-c3-idf.yaml b/tests/components/pulse_width/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/pulse_width/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml b/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/pvvx_mithermometer/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pylontech/test.esp32-c3-idf.yaml b/tests/components/pylontech/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pylontech/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pzem004t/test.esp32-c3-idf.yaml b/tests/components/pzem004t/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/pzem004t/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pzemac/test.esp32-c3-idf.yaml b/tests/components/pzemac/test.esp32-c3-idf.yaml deleted file mode 100644 index 6c6e95488fc..00000000000 --- a/tests/components/pzemac/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/pzemdc/test.esp32-c3-idf.yaml b/tests/components/pzemdc/test.esp32-c3-idf.yaml deleted file mode 100644 index 6c6e95488fc..00000000000 --- a/tests/components/pzemdc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/qmc5883l/test.esp32-c3-idf.yaml b/tests/components/qmc5883l/test.esp32-c3-idf.yaml deleted file mode 100644 index 854ddc25e72..00000000000 --- a/tests/components/qmc5883l/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - drdy_pin: GPIO6 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/qmp6988/test.esp32-c3-idf.yaml b/tests/components/qmp6988/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/qmp6988/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/qr_code/test.esp32-c3-idf.yaml b/tests/components/qr_code/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/qr_code/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/qwiic_pir/test.esp32-c3-idf.yaml b/tests/components/qwiic_pir/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/qwiic_pir/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml b/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/radon_eye_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml b/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/radon_eye_rd200/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/rc522_i2c/test.esp32-c3-idf.yaml b/tests/components/rc522_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/rc522_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/rc522_spi/test.esp32-c3-idf.yaml b/tests/components/rc522_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/rc522_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/rdm6300/test.esp32-c3-idf.yaml b/tests/components/rdm6300/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/rdm6300/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index e9593cc97ce..d48d36bd548 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -1,3 +1,11 @@ +number: + - platform: template + id: test_number + optimistic: true + min_value: 0 + max_value: 255 + step: 1 + button: - platform: template name: Beo4 audio mute @@ -128,10 +136,16 @@ button: address: 0x00 command: 0x0B - platform: template - name: RC5 Raw + name: RC5 Raw static on_press: remote_transmitter.transmit_raw: code: [1000, -1000] + - platform: template + name: RC5 Raw lambda + on_press: + remote_transmitter.transmit_raw: + code: !lambda |- + return {(int32_t)id(test_number).state * 100, -1000}; - platform: template name: AEHA id: eaha_hitachi_climate_power_on @@ -217,6 +231,23 @@ button: command: 0xEC rc_code_1: 0x0D rc_code_2: 0x0D + - platform: template + name: ABBWelcome static + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: [0x10, 0x20, 0x30] + - platform: template + name: ABBWelcome lambda + on_press: + remote_transmitter.transmit_abbwelcome: + source_address: 0x1234 + destination_address: 0x5678 + message_type: 0x01 + data: !lambda |- + return {(uint8_t)id(test_number).state, 0x20, 0x30}; - platform: template name: Digital Write on_press: diff --git a/tests/components/resampler/test.esp32-c3-idf.yaml b/tests/components/resampler/test.esp32-c3-idf.yaml deleted file mode 100644 index f1721f08625..00000000000 --- a/tests/components/resampler/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - lrclk_pin: GPIO4 - bclk_pin: GPIO5 - mclk_pin: GPIO6 - dout_pin: GPIO7 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-c3-idf.yaml b/tests/components/resistance/test.esp32-c3-idf.yaml deleted file mode 100644 index 37fb325f4a5..00000000000 --- a/tests/components/resistance/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/resistance/test.esp32-s3-idf.yaml b/tests/components/resistance/test.esp32-s3-idf.yaml deleted file mode 100644 index 1910f325ae5..00000000000 --- a/tests/components/resistance/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -substitutions: - pin: GPIO1 - -<<: !include common.yaml diff --git a/tests/components/restart/test.esp32-c3-idf.yaml b/tests/components/restart/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/restart/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/rf_bridge/test.esp32-c3-idf.yaml b/tests/components/rf_bridge/test.esp32-c3-idf.yaml deleted file mode 100644 index a19013bf544..00000000000 --- a/tests/components/rf_bridge/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/rgb/test.esp32-c3-idf.yaml b/tests/components/rgb/test.esp32-c3-idf.yaml deleted file mode 100644 index 1fe4a4bb909..00000000000 --- a/tests/components/rgb/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rgbct/test.esp32-c3-idf.yaml b/tests/components/rgbct/test.esp32-c3-idf.yaml deleted file mode 100644 index 27a1fbca4de..00000000000 --- a/tests/components/rgbct/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - pin5: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/rgbw/test.esp32-c3-idf.yaml b/tests/components/rgbw/test.esp32-c3-idf.yaml deleted file mode 100644 index b44734344eb..00000000000 --- a/tests/components/rgbw/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/rgbww/test.esp32-c3-idf.yaml b/tests/components/rgbww/test.esp32-c3-idf.yaml deleted file mode 100644 index 27a1fbca4de..00000000000 --- a/tests/components/rgbww/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - light_platform: ledc - pin1: GPIO2 - pin2: GPIO3 - pin3: GPIO4 - pin4: GPIO5 - pin5: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/rotary_encoder/test.esp32-c3-idf.yaml b/tests/components/rotary_encoder/test.esp32-c3-idf.yaml deleted file mode 100644 index b71a454bdd7..00000000000 --- a/tests/components/rotary_encoder/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - pin_a: GPIO2 - pin_b: GPIO3 - pin_reset: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/rp2040/test.rp2040-ard.yaml b/tests/components/rp2040/test.rp2040-ard.yaml new file mode 100644 index 00000000000..039a2610160 --- /dev/null +++ b/tests/components/rp2040/test.rp2040-ard.yaml @@ -0,0 +1,13 @@ +logger: + level: VERBOSE + +esphome: + on_boot: + - lambda: |- + int x = 100; + x = clamp(x, 50, 90); + assert(x == 90); + x = clamp_at_least(x, 95); + assert(x == 95); + x = clamp_at_most(x, 40); + assert(x == 40); diff --git a/tests/components/rtttl/test.esp32-c3-idf.yaml b/tests/components/rtttl/test.esp32-c3-idf.yaml deleted file mode 100644 index 74769635918..00000000000 --- a/tests/components/rtttl/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml b/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ruuvi_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ruuvitag/test.esp32-c3-idf.yaml b/tests/components/ruuvitag/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/ruuvitag/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/rx8130/common.yaml b/tests/components/rx8130/common.yaml new file mode 100644 index 00000000000..e6b849e25bd --- /dev/null +++ b/tests/components/rx8130/common.yaml @@ -0,0 +1,8 @@ +esphome: + on_boot: + - rx8130.read_time + - rx8130.write_time + +time: + - platform: rx8130 + i2c_id: i2c_bus diff --git a/tests/components/rx8130/test.esp32-idf.yaml b/tests/components/rx8130/test.esp32-idf.yaml new file mode 100644 index 00000000000..b47e39c3898 --- /dev/null +++ b/tests/components/rx8130/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/rx8130/test.esp8266-ard.yaml b/tests/components/rx8130/test.esp8266-ard.yaml new file mode 100644 index 00000000000..4a98b9388ab --- /dev/null +++ b/tests/components/rx8130/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/rx8130/test.nrf52-adafruit.yaml b/tests/components/rx8130/test.nrf52-adafruit.yaml new file mode 100644 index 00000000000..2a0de6241c3 --- /dev/null +++ b/tests/components/rx8130/test.nrf52-adafruit.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/rx8130/test.rp2040-ard.yaml b/tests/components/rx8130/test.rp2040-ard.yaml new file mode 100644 index 00000000000..319a7c71a65 --- /dev/null +++ b/tests/components/rx8130/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/safe_mode/test-enabled.esp32-c3-idf.yaml b/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml deleted file mode 100644 index 97fd63d70e3..00000000000 --- a/tests/components/safe_mode/test-enabled.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common-enabled.yaml diff --git a/tests/components/scd30/test.esp32-c3-idf.yaml b/tests/components/scd30/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/scd30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/scd4x/test.esp32-c3-idf.yaml b/tests/components/scd4x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/scd4x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/script/test.esp32-c3-idf.yaml b/tests/components/script/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/script/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sdl/common.yaml b/tests/components/sdl/common.yaml index 52991d595c0..66f93915b6f 100644 --- a/tests/components/sdl/common.yaml +++ b/tests/components/sdl/common.yaml @@ -13,11 +13,14 @@ display: binary_sensor: - platform: sdl + sdl_id: sdl_display id: key_up key: SDLK_UP - platform: sdl + sdl_id: sdl_display id: key_down key: SDLK_DOWN - platform: sdl + sdl_id: sdl_display id: key_enter key: SDLK_RETURN diff --git a/tests/components/sdm_meter/test.esp32-c3-idf.yaml b/tests/components/sdm_meter/test.esp32-c3-idf.yaml deleted file mode 100644 index 6c6e95488fc..00000000000 --- a/tests/components/sdm_meter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sdp3x/test.esp32-c3-idf.yaml b/tests/components/sdp3x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sdp3x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sds011/test.esp32-c3-idf.yaml b/tests/components/sds011/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sds011/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml b/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml deleted file mode 100644 index a19013bf544..00000000000 --- a/tests/components/seeed_mr24hpc1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml b/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml deleted file mode 100644 index fea89f57684..00000000000 --- a/tests/components/seeed_mr60bha2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_115200: !include ../../test_build_components/common/uart_115200/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml b/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml deleted file mode 100644 index fea89f57684..00000000000 --- a/tests/components/seeed_mr60fda2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_115200: !include ../../test_build_components/common/uart_115200/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/selec_meter/test.esp32-c3-idf.yaml b/tests/components/selec_meter/test.esp32-c3-idf.yaml deleted file mode 100644 index beb90e14710..00000000000 --- a/tests/components/selec_meter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - flow_control_pin: GPIO10 - -packages: - modbus: !include ../../test_build_components/common/modbus/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sen0321/test.esp32-c3-idf.yaml b/tests/components/sen0321/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sen0321/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sen21231/test.esp32-c3-idf.yaml b/tests/components/sen21231/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sen21231/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sen5x/test.esp32-c3-idf.yaml b/tests/components/sen5x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sen5x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/senseair/test.esp32-c3-idf.yaml b/tests/components/senseair/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/senseair/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/servo/test.esp32-c3-idf.yaml b/tests/components/servo/test.esp32-c3-idf.yaml deleted file mode 100644 index 74769635918..00000000000 --- a/tests/components/servo/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sfa30/test.esp32-c3-idf.yaml b/tests/components/sfa30/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sfa30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sgp30/test.esp32-c3-idf.yaml b/tests/components/sgp30/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sgp30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sgp4x/test.esp32-c3-idf.yaml b/tests/components/sgp4x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sgp4x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sht3xd/test.esp32-c3-idf.yaml b/tests/components/sht3xd/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sht3xd/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sht4x/test.esp32-c3-idf.yaml b/tests/components/sht4x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sht4x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/shtcx/test.esp32-c3-idf.yaml b/tests/components/shtcx/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/shtcx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/shutdown/test.esp32-c3-idf.yaml b/tests/components/shutdown/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/shutdown/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sim800l/test.esp32-c3-idf.yaml b/tests/components/sim800l/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sim800l/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/slow_pwm/test.esp32-c3-idf.yaml b/tests/components/slow_pwm/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/slow_pwm/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sm16716/test.esp32-c3-idf.yaml b/tests/components/sm16716/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/sm16716/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2135/test.esp32-c3-idf.yaml b/tests/components/sm2135/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/sm2135/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2235/test.esp32-c3-idf.yaml b/tests/components/sm2235/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/sm2235/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm2335/test.esp32-c3-idf.yaml b/tests/components/sm2335/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/sm2335/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/sm300d2/test.esp32-c3-idf.yaml b/tests/components/sm300d2/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sm300d2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sml/test.esp32-c3-idf.yaml b/tests/components/sml/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sml/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/smt100/test.esp32-c3-idf.yaml b/tests/components/smt100/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/smt100/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sn74hc165/test.esp32-c3-idf.yaml b/tests/components/sn74hc165/test.esp32-c3-idf.yaml deleted file mode 100644 index 0a3db917b78..00000000000 --- a/tests/components/sn74hc165/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO3 - data_pin: GPIO4 - load_pin: GPIO5 - clock_inhibit_pin: GPIO6 - -<<: !include common.yaml diff --git a/tests/components/sn74hc595/test.esp32-c3-idf.yaml b/tests/components/sn74hc595/test.esp32-c3-idf.yaml deleted file mode 100644 index 74b5e855fa6..00000000000 --- a/tests/components/sn74hc595/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,12 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -substitutions: - clock_pin: GPIO7 - data_pin: GPIO10 - latch_pin1: GPIO1 - oe_pin1: GPIO2 - latch_pin2: GPIO3 - oe_pin2: GPIO9 - -<<: !include common.yaml diff --git a/tests/components/sntp/test.esp32-c3-idf.yaml b/tests/components/sntp/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/sntp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/socket/conftest.py b/tests/components/socket/conftest.py new file mode 100644 index 00000000000..5d93cac232a --- /dev/null +++ b/tests/components/socket/conftest.py @@ -0,0 +1,12 @@ +"""Configuration file for socket component tests.""" + +import pytest + +from esphome.core import CORE + + +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE after each test.""" + yield + CORE.reset() diff --git a/tests/components/socket/test_wake_loop_threadsafe.py b/tests/components/socket/test_wake_loop_threadsafe.py new file mode 100644 index 00000000000..45e5ea22115 --- /dev/null +++ b/tests/components/socket/test_wake_loop_threadsafe.py @@ -0,0 +1,42 @@ +from esphome.components import socket +from esphome.core import CORE + + +def test_require_wake_loop_threadsafe__first_call() -> None: + """Test that first call sets up define and consumes socket.""" + 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 + + # 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.""" + # Call three times + 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) diff --git a/tests/components/sonoff_d1/test.esp32-c3-idf.yaml b/tests/components/sonoff_d1/test.esp32-c3-idf.yaml deleted file mode 100644 index a19013bf544..00000000000 --- a/tests/components/sonoff_d1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sound_level/test.esp32-c3-idf.yaml b/tests/components/sound_level/test.esp32-c3-idf.yaml deleted file mode 100644 index aeb7d9f0af5..00000000000 --- a/tests/components/sound_level/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO6 - i2s_lrclk_pin: GPIO7 - i2s_dout_pin: GPIO8 - -<<: !include common.yaml diff --git a/tests/components/speaker/audio_dac.esp32-c3-idf.yaml b/tests/components/speaker/audio_dac.esp32-c3-idf.yaml deleted file mode 100644 index 30900f19201..00000000000 --- a/tests/components/speaker/audio_dac.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - i2s_bclk_pin: GPIO7 - i2s_lrclk_pin: GPIO6 - i2s_mclk_pin: GPIO9 - i2s_dout_pin: GPIO8 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common-audio_dac.yaml diff --git a/tests/components/speaker/common.yaml b/tests/components/speaker/common.yaml index c04674ee29c..9aaf639162b 100644 --- a/tests/components/speaker/common.yaml +++ b/tests/components/speaker/common.yaml @@ -1,18 +1,52 @@ +number: + - platform: template + name: "Speaker Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + esphome: on_boot: then: - speaker.mute_on: + id: speaker_id - speaker.mute_off: + id: speaker_id - if: - condition: speaker.is_stopped + condition: + speaker.is_stopped: + id: speaker_id then: - - speaker.play: [0, 1, 2, 3] - - speaker.volume_set: 0.9 + - speaker.play: + id: speaker_id + data: [0, 1, 2, 3] + - speaker.volume_set: + id: speaker_id + volume: 0.9 - if: - condition: speaker.is_playing + condition: + speaker.is_playing: + id: speaker_id then: - speaker.finish: + id: speaker_id - speaker.stop: + id: speaker_id + +button: + - platform: template + name: "Speaker Button" + on_press: + then: + - speaker.play: + id: speaker_id + data: [0x10, 0x20, 0x30, 0x40] + - speaker.play: + id: speaker_id + data: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; i2s_audio: i2s_lrclk_pin: ${i2s_bclk_pin} diff --git a/tests/components/speaker/test.esp32-c3-idf.yaml b/tests/components/speaker/test.esp32-c3-idf.yaml deleted file mode 100644 index 9d1a1cca3bf..00000000000 --- a/tests/components/speaker/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,12 +0,0 @@ -substitutions: - scl_pin: GPIO5 - sda_pin: GPIO4 - i2s_bclk_pin: GPIO7 - i2s_lrclk_pin: GPIO6 - i2s_mclk_pin: GPIO9 - i2s_dout_pin: GPIO8 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/speed/test.esp32-c3-idf.yaml b/tests/components/speed/test.esp32-c3-idf.yaml deleted file mode 100644 index 74769635918..00000000000 --- a/tests/components/speed/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - output_platform: ledc - pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/spi_device/test.esp32-c3-idf.yaml b/tests/components/spi_device/test.esp32-c3-idf.yaml deleted file mode 100644 index 6d64c2b23bb..00000000000 --- a/tests/components/spi_device/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/spi_led_strip/test.esp32-c3-idf.yaml b/tests/components/spi_led_strip/test.esp32-c3-idf.yaml deleted file mode 100644 index 6d64c2b23bb..00000000000 --- a/tests/components/spi_led_strip/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sprinkler/test.esp32-c3-idf.yaml b/tests/components/sprinkler/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/sprinkler/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sps30/test.esp32-c3-idf.yaml b/tests/components/sps30/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sps30/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml b/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index f8bfab23198..00000000000 --- a/tests/components/ssd1306_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml b/tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml new file mode 100644 index 00000000000..28254e4af59 --- /dev/null +++ b/tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml @@ -0,0 +1,7 @@ +substitutions: + reset_pin: P0.10 + +packages: + i2c: !include ../../test_build_components/common/i2c/nrf52.yaml + +<<: !include common.yaml diff --git a/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1306_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1322_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1325_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml b/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index f8bfab23198..00000000000 --- a/tests/components/ssd1327_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1327_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1331_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml b/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/ssd1351_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7567_i2c/test.esp32-c3-idf.yaml b/tests/components/st7567_i2c/test.esp32-c3-idf.yaml deleted file mode 100644 index f8bfab23198..00000000000 --- a/tests/components/st7567_i2c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7567_spi/test.esp32-c3-idf.yaml b/tests/components/st7567_spi/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/st7567_spi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7735/test.esp32-c3-idf.yaml b/tests/components/st7735/test.esp32-c3-idf.yaml deleted file mode 100644 index b112cf4c31d..00000000000 --- a/tests/components/st7735/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7789v/test.esp32-c3-idf.yaml b/tests/components/st7789v/test.esp32-c3-idf.yaml deleted file mode 100644 index b4d70edb318..00000000000 --- a/tests/components/st7789v/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - cs_pin: GPIO8 - dc_pin: GPIO9 - reset_pin: GPIO10 - backlight_pin: GPIO7 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/st7920/test.esp32-c3-idf.yaml b/tests/components/st7920/test.esp32-c3-idf.yaml deleted file mode 100644 index 15d986e157c..00000000000 --- a/tests/components/st7920/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO8 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/statsD/test.esp32-c3-idf.yaml b/tests/components/statsD/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/statsD/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/status/test.esp32-c3-idf.yaml b/tests/components/status/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/status/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/status_led/test.esp32-c3-idf.yaml b/tests/components/status_led/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/status_led/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/stepper/test.esp32-c3-idf.yaml b/tests/components/stepper/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/stepper/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sts3x/test.esp32-c3-idf.yaml b/tests/components/sts3x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sts3x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sun/test.esp32-c3-idf.yaml b/tests/components/sun/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/sun/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/sun_gtil2/test.esp32-c3-idf.yaml b/tests/components/sun_gtil2/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/sun_gtil2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/switch/test.esp32-c3-idf.yaml b/tests/components/switch/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/switch/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/switch/test.esp32-s3-idf.yaml b/tests/components/switch/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/switch/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/sx126x/common.yaml b/tests/components/sx126x/common.yaml index 3f540a4baed..659550cc01d 100644 --- a/tests/components/sx126x/common.yaml +++ b/tests/components/sx126x/common.yaml @@ -26,6 +26,15 @@ sx126x: - lambda: |- ESP_LOGD("lambda", "packet %.2f %.2f %s", rssi, snr, format_hex(x).c_str()); +number: + - platform: template + name: "SX126x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX126x Button" @@ -37,3 +46,5 @@ button: - sx126x.set_mode_rx - sx126x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx126x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; diff --git a/tests/components/sx126x/test.esp32-c3-idf.yaml b/tests/components/sx126x/test.esp32-c3-idf.yaml deleted file mode 100644 index e27f11032e1..00000000000 --- a/tests/components/sx126x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - cs_pin: GPIO1 - rst_pin: GPIO2 - busy_pin: GPIO7 - dio1_pin: GPIO3 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sx127x/common.yaml b/tests/components/sx127x/common.yaml index 540381fc083..6e48952fcca 100644 --- a/tests/components/sx127x/common.yaml +++ b/tests/components/sx127x/common.yaml @@ -26,6 +26,15 @@ sx127x: - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] +number: + - platform: template + name: "SX127x Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: - platform: template name: "SX127x Button" @@ -38,3 +47,5 @@ button: - sx127x.set_mode_rx - sx127x.send_packet: data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C] + - sx127x.send_packet: !lambda |- + return {0x01, 0x02, (uint8_t)id(my_number).state}; diff --git a/tests/components/sx127x/test.esp32-c3-idf.yaml b/tests/components/sx127x/test.esp32-c3-idf.yaml deleted file mode 100644 index dfee192545e..00000000000 --- a/tests/components/sx127x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - cs_pin: GPIO1 - rst_pin: GPIO2 - dio0_pin: GPIO3 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/sx1509/test.esp32-c3-idf.yaml b/tests/components/sx1509/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/sx1509/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/syslog/test.esp32-c3-idf.yaml b/tests/components/syslog/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/syslog/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/t6615/test.esp32-c3-idf.yaml b/tests/components/t6615/test.esp32-c3-idf.yaml deleted file mode 100644 index 147d967dd43..00000000000 --- a/tests/components/t6615/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart_19200: !include ../../test_build_components/common/uart_19200/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tc74/test.esp32-c3-idf.yaml b/tests/components/tc74/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tc74/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tca9548a/test.esp32-c3-idf.yaml b/tests/components/tca9548a/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tca9548a/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tca9555/test.esp32-c3-idf.yaml b/tests/components/tca9555/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tca9555/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tcl112/test.esp32-c3-idf.yaml b/tests/components/tcl112/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/tcl112/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tcs34725/test.esp32-c3-idf.yaml b/tests/components/tcs34725/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tcs34725/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tee501/test.esp32-c3-idf.yaml b/tests/components/tee501/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tee501/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/teleinfo/test.esp32-c3-idf.yaml b/tests/components/teleinfo/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/teleinfo/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tem3200/test.esp32-s3-idf.yaml b/tests/components/tem3200/test.esp32-s3-idf.yaml deleted file mode 100644 index e9d826aa7c7..00000000000 --- a/tests/components/tem3200/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/template/test.esp32-c3-idf.yaml b/tests/components/template/test.esp32-c3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/template/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/template/test.esp32-s3-idf.yaml b/tests/components/template/test.esp32-s3-idf.yaml deleted file mode 100644 index 25cb37a0b42..00000000000 --- a/tests/components/template/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,2 +0,0 @@ -packages: - common: !include common.yaml diff --git a/tests/components/thermostat/test.esp32-c3-idf.yaml b/tests/components/thermostat/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/thermostat/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/time/test.esp32-c3-idf.yaml b/tests/components/time/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/time/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/time_based/test.esp32-c3-idf.yaml b/tests/components/time_based/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/time_based/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tinyusb/common.yaml b/tests/components/tinyusb/common.yaml new file mode 100644 index 00000000000..cb3f48836ab --- /dev/null +++ b/tests/components/tinyusb/common.yaml @@ -0,0 +1,8 @@ +tinyusb: + id: tinyusb_test + usb_lang_id: 0x0123 + usb_manufacturer_str: ESPHomeTestManufacturer + usb_product_id: 0x1234 + usb_product_str: ESPHomeTestProduct + usb_serial_str: ESPHomeTestSerialNumber + usb_vendor_id: 0x2345 diff --git a/tests/components/alarm_control_panel/test.esp32-c3-idf.yaml b/tests/components/tinyusb/test.esp32-p4-idf.yaml similarity index 100% rename from tests/components/alarm_control_panel/test.esp32-c3-idf.yaml rename to tests/components/tinyusb/test.esp32-p4-idf.yaml diff --git a/tests/components/analog_threshold/test.esp32-c3-idf.yaml b/tests/components/tinyusb/test.esp32-s2-idf.yaml similarity index 100% rename from tests/components/analog_threshold/test.esp32-c3-idf.yaml rename to tests/components/tinyusb/test.esp32-s2-idf.yaml diff --git a/tests/components/debug/test.esp32-s3-idf.yaml b/tests/components/tinyusb/test.esp32-s3-idf.yaml similarity index 100% rename from tests/components/debug/test.esp32-s3-idf.yaml rename to tests/components/tinyusb/test.esp32-s3-idf.yaml diff --git a/tests/components/tlc59208f/test.esp32-c3-idf.yaml b/tests/components/tlc59208f/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tlc59208f/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tlc5947/test.esp32-c3-idf.yaml b/tests/components/tlc5947/test.esp32-c3-idf.yaml deleted file mode 100644 index 4694c43642c..00000000000 --- a/tests/components/tlc5947/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - lat_pin: GPIO3 - -packages: - common: !include common.yaml diff --git a/tests/components/tlc5971/test.esp32-c3-idf.yaml b/tests/components/tlc5971/test.esp32-c3-idf.yaml deleted file mode 100644 index d898a21d469..00000000000 --- a/tests/components/tlc5971/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -packages: - common: !include common.yaml diff --git a/tests/components/tm1621/test.esp32-c3-idf.yaml b/tests/components/tm1621/test.esp32-c3-idf.yaml deleted file mode 100644 index 562ced7485c..00000000000 --- a/tests/components/tm1621/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - cs_pin: GPIO6 - data_pin: GPIO7 - read_pin: GPIO2 - write_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tm1637/test.esp32-c3-idf.yaml b/tests/components/tm1637/test.esp32-c3-idf.yaml deleted file mode 100644 index 0c4d4a9a7a4..00000000000 --- a/tests/components/tm1637/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clk_pin: GPIO7 - dio_pin: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/tm1638/test.esp32-c3-idf.yaml b/tests/components/tm1638/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/tm1638/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tm1651/test.esp32-c3-idf.yaml b/tests/components/tm1651/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/tm1651/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/tmp102/test.esp32-c3-idf.yaml b/tests/components/tmp102/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tmp102/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tmp1075/test.esp32-c3-idf.yaml b/tests/components/tmp1075/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tmp1075/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tmp117/test.esp32-c3-idf.yaml b/tests/components/tmp117/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tmp117/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tof10120/test.esp32-c3-idf.yaml b/tests/components/tof10120/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tof10120/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tormatic/test.esp32-c3-idf.yaml b/tests/components/tormatic/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/tormatic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/toshiba/common_ras2819t.yaml b/tests/components/toshiba/common_ras2819t.yaml index 32081fca98e..157456ba81b 100644 --- a/tests/components/toshiba/common_ras2819t.yaml +++ b/tests/components/toshiba/common_ras2819t.yaml @@ -1,11 +1,3 @@ -remote_transmitter: - pin: ${tx_pin} - carrier_duty_percent: 50% - -remote_receiver: - id: rcvr - pin: ${rx_pin} - climate: - platform: toshiba name: "RAS-2819T Climate" diff --git a/tests/components/toshiba/test.esp32-c3-idf.yaml b/tests/components/toshiba/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/toshiba/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-ard.yaml b/tests/components/toshiba/test_ras2819t.esp32-ard.yaml index 00805baa011..d82ba54897c 100644 --- a/tests/components/toshiba/test_ras2819t.esp32-ard.yaml +++ b/tests/components/toshiba/test_ras2819t.esp32-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-ard.yaml <<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml b/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml index 00805baa011..6858dd587f4 100644 --- a/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml +++ b/tests/components/toshiba/test_ras2819t.esp32-c3-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-c3-ard.yaml <<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-c3-idf.yaml b/tests/components/toshiba/test_ras2819t.esp32-c3-idf.yaml deleted file mode 100644 index 00805baa011..00000000000 --- a/tests/components/toshiba/test_ras2819t.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 - -<<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp32-idf.yaml b/tests/components/toshiba/test_ras2819t.esp32-idf.yaml index 00805baa011..3facc5bbb34 100644 --- a/tests/components/toshiba/test_ras2819t.esp32-idf.yaml +++ b/tests/components/toshiba/test_ras2819t.esp32-idf.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml <<: !include common_ras2819t.yaml diff --git a/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml b/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml index 00805baa011..3976dcc739e 100644 --- a/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml +++ b/tests/components/toshiba/test_ras2819t.esp8266-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO5 - rx_pin: GPIO4 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml <<: !include common_ras2819t.yaml diff --git a/tests/components/total_daily_energy/test.esp32-c3-idf.yaml b/tests/components/total_daily_energy/test.esp32-c3-idf.yaml deleted file mode 100644 index 8b0d069ce2e..00000000000 --- a/tests/components/total_daily_energy/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - sel_pin: GPIO2 - cf_pin: GPIO3 - cf1_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/components/tsl2561/test.esp32-c3-idf.yaml b/tests/components/tsl2561/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tsl2561/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tsl2591/test.esp32-c3-idf.yaml b/tests/components/tsl2591/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/tsl2591/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tt21100/test.esp32-c3-idf.yaml b/tests/components/tt21100/test.esp32-c3-idf.yaml deleted file mode 100644 index a7265e10b2c..00000000000 --- a/tests/components/tt21100/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - disp_reset_pin: GPIO7 - interrupt_pin: GPIO2 - reset_pin: GPIO3 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml b/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml deleted file mode 100644 index ad1c58b40e0..00000000000 --- a/tests/components/ttp229_bsf/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - ttp229_scl_pin: GPIO7 - ttp229_sdo_pin: GPIO4 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml b/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ttp229_lsf/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tuya/test.esp32-c3-idf.yaml b/tests/components/tuya/test.esp32-c3-idf.yaml deleted file mode 100644 index 43c28ba7b31..00000000000 --- a/tests/components/tuya/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - status_pin: GPIO2 -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/tx20/test.esp32-c3-idf.yaml b/tests/components/tx20/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/tx20/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml b/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml deleted file mode 100644 index 88a806eb926..00000000000 --- a/tests/components/uart/test-uart_max_with_usb_serial_jtag.esp32-s3-idf.yaml +++ /dev/null @@ -1,48 +0,0 @@ -<<: !include ../logger/common-usb_serial_jtag.yaml - -esphome: - on_boot: - then: - - uart.write: - id: uart_1 - data: 'Hello World' - - uart.write: - id: uart_1 - data: [0x00, 0x20, 0x42] - -uart: - - id: uart_1 - tx_pin: 4 - rx_pin: 5 - flow_control_pin: 6 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - rx_full_threshold: 10 - rx_timeout: 1 - parity: EVEN - stop_bits: 2 - - - id: uart_2 - tx_pin: 7 - rx_pin: 8 - flow_control_pin: 9 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - rx_full_threshold: 10 - rx_timeout: 1 - parity: EVEN - stop_bits: 2 - - - id: uart_3 - tx_pin: 10 - rx_pin: 11 - flow_control_pin: 12 - baud_rate: 9600 - data_bits: 8 - rx_buffer_size: 512 - rx_full_threshold: 10 - rx_timeout: 1 - parity: EVEN - stop_bits: 2 diff --git a/tests/components/uart/test.esp32-idf.yaml b/tests/components/uart/test.esp32-idf.yaml index 9744a484096..6ffd0d72824 100644 --- a/tests/components/uart/test.esp32-idf.yaml +++ b/tests/components/uart/test.esp32-idf.yaml @@ -3,6 +3,8 @@ esphome: then: - uart.write: 'Hello World' - uart.write: [0x00, 0x20, 0x42] + - uart.write: !lambda |- + return {0xAA, 0xBB, 0xCC}; uart: - id: uart_uart @@ -46,6 +48,15 @@ switch: turn_on: "TURN_ON" turn_off: "TURN_OFF" +number: + - platform: template + name: "Test Number" + id: test_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + button: # Test uart button with array data - platform: uart @@ -57,3 +68,10 @@ button: name: "UART Button String" uart_id: uart_uart data: "BUTTON_PRESS" + # Test uart button with lambda (function pointer) + - platform: template + name: "UART Lambda Test" + on_press: + - uart.write: !lambda |- + std::string cmd = "VALUE=" + str_sprintf("%.0f", id(test_number).state) + "\r\n"; + return std::vector(cmd.begin(), cmd.end()); diff --git a/tests/components/udp/common.yaml b/tests/components/udp/common.yaml index 96224d0d1f1..98546d49ef5 100644 --- a/tests/components/udp/common.yaml +++ b/tests/components/udp/common.yaml @@ -17,3 +17,22 @@ udp: id: my_udp data: !lambda |- return std::vector{1,3,4,5,6}; + +number: + - platform: template + name: "UDP Number" + id: my_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +button: + - platform: template + name: "UDP Button" + on_press: + then: + - udp.write: + data: [0x01, 0x02, 0x03] + - udp.write: !lambda |- + return {0x10, 0x20, (uint8_t)id(my_number).state}; diff --git a/tests/components/udp/test.esp32-c3-idf.yaml b/tests/components/udp/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/udp/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ufire_ec/test.esp32-c3-idf.yaml b/tests/components/ufire_ec/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ufire_ec/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/ufire_ise/test.esp32-c3-idf.yaml b/tests/components/ufire_ise/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/ufire_ise/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/uln2003/test.esp32-c3-idf.yaml b/tests/components/uln2003/test.esp32-c3-idf.yaml deleted file mode 100644 index 11d16a4d5da..00000000000 --- a/tests/components/uln2003/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,7 +0,0 @@ -substitutions: - pin_a: GPIO0 - pin_b: GPIO1 - pin_c: GPIO2 - pin_d: GPIO3 - -<<: !include common.yaml diff --git a/tests/components/ultrasonic/test.esp32-c3-idf.yaml b/tests/components/ultrasonic/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/ultrasonic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml b/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml deleted file mode 100644 index cd26c783c29..00000000000 --- a/tests/components/uponor_smatrix/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -packages: - uart_19200: !include ../../test_build_components/common/uart_19200/esp32-c3-idf.yaml - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/uptime/test.esp32-c3-idf.yaml b/tests/components/uptime/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/uptime/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/vbus/test.esp32-c3-idf.yaml b/tests/components/vbus/test.esp32-c3-idf.yaml deleted file mode 100644 index a19013bf544..00000000000 --- a/tests/components/vbus/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/veml3235/test.esp32-c3-idf.yaml b/tests/components/veml3235/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/veml3235/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/veml7700/test.esp32-c3-idf.yaml b/tests/components/veml7700/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/veml7700/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/version/test.esp32-c3-idf.yaml b/tests/components/version/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/version/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/vl53l0x/test.esp32-c3-idf.yaml b/tests/components/vl53l0x/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/vl53l0x/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/voice_assistant/test.esp32-c3-idf.yaml b/tests/components/voice_assistant/test.esp32-c3-idf.yaml deleted file mode 100644 index 46745e4308c..00000000000 --- a/tests/components/voice_assistant/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,8 +0,0 @@ -substitutions: - i2s_lrclk_pin: GPIO6 - i2s_bclk_pin: GPIO7 - i2s_mclk_pin: GPIO5 - i2s_din_pin: GPIO3 - i2s_dout_pin: GPIO2 - -<<: !include common-idf.yaml diff --git a/tests/components/wake_on_lan/test.esp32-c3-idf.yaml b/tests/components/wake_on_lan/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/wake_on_lan/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml b/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml deleted file mode 100644 index bdd7e1d3509..00000000000 --- a/tests/components/waveshare_epaper/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - cs_pin: GPIO7 - dc_pin: GPIO1 - busy_pin: GPIO2 - reset_pin: GPIO3 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/web_server/test.esp32-c3-idf.yaml b/tests/components/web_server/test.esp32-c3-idf.yaml deleted file mode 100644 index 7e6658e20e3..00000000000 --- a/tests/components/web_server/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common_v2.yaml diff --git a/tests/components/whirlpool/test.esp32-c3-idf.yaml b/tests/components/whirlpool/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/whirlpool/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/whynter/test.esp32-c3-idf.yaml b/tests/components/whynter/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/whynter/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wiegand/test.esp32-c3-idf.yaml b/tests/components/wiegand/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/wiegand/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index af27f850923..5d9973cbc80 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -15,5 +15,10 @@ wifi: networks: - ssid: MySSID password: password1 + priority: 10 - ssid: MySSID2 password: password2 + priority: 5 + - ssid: MySSID3 + password: password3 + priority: 0 diff --git a/tests/components/wifi/test.esp32-c3-idf.yaml b/tests/components/wifi/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/wifi/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wifi/test.esp32-idf.yaml b/tests/components/wifi/test.esp32-idf.yaml index 91e235b9ce2..6b3ef20963c 100644 --- a/tests/components/wifi/test.esp32-idf.yaml +++ b/tests/components/wifi/test.esp32-idf.yaml @@ -2,6 +2,22 @@ psram: wifi: use_psram: true + min_auth_mode: WPA + manual_ip: + static_ip: 192.168.1.100 + gateway: 192.168.1.1 + subnet: 255.255.255.0 + dns1: 1.1.1.1 + dns2: 8.8.8.8 + ap: + ssid: Fallback AP + password: fallback_password + manual_ip: + static_ip: 192.168.4.1 + gateway: 192.168.4.1 + subnet: 255.255.255.0 + +captive_portal: packages: - !include common.yaml diff --git a/tests/components/wifi/test.esp8266-ard.yaml b/tests/components/wifi/test.esp8266-ard.yaml index dade44d145b..9cb0e3cf48e 100644 --- a/tests/components/wifi/test.esp8266-ard.yaml +++ b/tests/components/wifi/test.esp8266-ard.yaml @@ -1 +1,5 @@ -<<: !include common.yaml +wifi: + min_auth_mode: WPA2 + +packages: + - !include common.yaml diff --git a/tests/components/wifi_info/test.esp32-c3-idf.yaml b/tests/components/wifi_info/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/wifi_info/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wifi_signal/test.esp32-c3-idf.yaml b/tests/components/wifi_signal/test.esp32-c3-idf.yaml deleted file mode 100644 index dade44d145b..00000000000 --- a/tests/components/wifi_signal/test.esp32-c3-idf.yaml +++ /dev/null @@ -1 +0,0 @@ -<<: !include common.yaml diff --git a/tests/components/wireguard/test.esp32-c3-idf.yaml b/tests/components/wireguard/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/wireguard/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index d7b149a6fdc..00000000000 --- a/tests/components/wk2132_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2132_spi/test.esp32-s3-idf.yaml b/tests/components/wk2132_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 9c7d36996e2..00000000000 --- a/tests/components/wk2132_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 115812be973..00000000000 --- a/tests/components/wk2168_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2168_spi/test.esp32-s3-idf.yaml b/tests/components/wk2168_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 374fe64d166..00000000000 --- a/tests/components/wk2168_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 115812be973..00000000000 --- a/tests/components/wk2204_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2204_spi/test.esp32-s3-idf.yaml b/tests/components/wk2204_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 374fe64d166..00000000000 --- a/tests/components/wk2204_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml b/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml deleted file mode 100644 index 115812be973..00000000000 --- a/tests/components/wk2212_i2c/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,9 +0,0 @@ -substitutions: - scl_pin: GPIO40 - sda_pin: GPIO41 - -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wk2212_spi/test.esp32-s3-idf.yaml b/tests/components/wk2212_spi/test.esp32-s3-idf.yaml deleted file mode 100644 index 374fe64d166..00000000000 --- a/tests/components/wk2212_spi/test.esp32-s3-idf.yaml +++ /dev/null @@ -1,11 +0,0 @@ -substitutions: - clk_pin: GPIO40 - miso_pin: GPIO41 - mosi_pin: GPIO6 - cs_pin: GPIO19 - -packages: - spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml - uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wl_134/test.esp32-c3-idf.yaml b/tests/components/wl_134/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/wl_134/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/wts01/test.esp32-c3-idf.yaml b/tests/components/wts01/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/wts01/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/x9c/test.esp32-c3-idf.yaml b/tests/components/x9c/test.esp32-c3-idf.yaml deleted file mode 100644 index b06e15a98c3..00000000000 --- a/tests/components/x9c/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,6 +0,0 @@ -substitutions: - cs_pin: GPIO3 - inc_pin: GPIO4 - ud_pin: GPIO5 - -<<: !include common.yaml diff --git a/tests/components/xgzp68xx/test.esp32-c3-idf.yaml b/tests/components/xgzp68xx/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/xgzp68xx/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml b/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_ble/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_cgd1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_cgdk2/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_cgg1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml b/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_cgpr1/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml b/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_gcls002/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml b/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_hhccjcy01/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml b/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_hhccpot002/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml b/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_jqjcy01ym/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_lywsd02/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_lywsd02mmc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_lywsd03mmc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml b/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_lywsdcgq/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_mhoc303/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_mhoc401/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml b/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_miscale/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_mjyd02yla/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml b/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_mue4094rt/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml b/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_rtcgq02lm/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml b/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_wx08zm/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml b/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml deleted file mode 100644 index 9f2634f967c..00000000000 --- a/tests/components/xiaomi_xmwsdj04mmc/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - ble: !include ../../test_build_components/common/ble/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xl9535/test.esp32-c3-idf.yaml b/tests/components/xl9535/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/xl9535/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/xpt2046/test.esp32-c3-idf.yaml b/tests/components/xpt2046/test.esp32-c3-idf.yaml deleted file mode 100644 index ff7a32c26c0..00000000000 --- a/tests/components/xpt2046/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,10 +0,0 @@ -substitutions: - dc_pin: GPIO7 - cs_pin: GPIO0 - disp_cs_pin: GPIO1 - interrupt_pin: GPIO3 - reset_pin: GPIO10 -packages: - spi: !include ../../test_build_components/common/spi/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/yashima/test.esp32-c3-idf.yaml b/tests/components/yashima/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/yashima/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/zhlt01/test.esp32-c3-idf.yaml b/tests/components/zhlt01/test.esp32-c3-idf.yaml deleted file mode 100644 index 43d5343715e..00000000000 --- a/tests/components/zhlt01/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml b/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml deleted file mode 100644 index 9990d96d290..00000000000 --- a/tests/components/zio_ultrasonic/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -packages: - i2c: !include ../../test_build_components/common/i2c/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/zwave_proxy/test.esp32-c3-idf.yaml b/tests/components/zwave_proxy/test.esp32-c3-idf.yaml deleted file mode 100644 index 4b7c8351a73..00000000000 --- a/tests/components/zwave_proxy/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: -packages: - uart: !include ../../test_build_components/common/uart/esp32-c3-idf.yaml - -<<: !include common.yaml diff --git a/tests/components/zyaura/test.esp32-c3-idf.yaml b/tests/components/zyaura/test.esp32-c3-idf.yaml deleted file mode 100644 index 78084812157..00000000000 --- a/tests/components/zyaura/test.esp32-c3-idf.yaml +++ /dev/null @@ -1,5 +0,0 @@ -substitutions: - clock_pin: GPIO5 - data_pin: GPIO4 - -<<: !include common.yaml diff --git a/tests/integration/fixtures/api_custom_services.yaml b/tests/integration/fixtures/api_custom_services.yaml index 41efc95b854..a597c741267 100644 --- a/tests/integration/fixtures/api_custom_services.yaml +++ b/tests/integration/fixtures/api_custom_services.yaml @@ -11,6 +11,28 @@ api: then: - logger.log: "YAML service called" + # Test YAML service with arguments (tests UserServiceBase with const char* array) + - action: test_yaml_service_with_args + variables: + my_int: int + my_string: string + then: + - logger.log: + format: "YAML service with args: %d, %s" + args: [my_int, my_string.c_str()] + + # Test YAML service with multiple arguments + - action: test_yaml_service_many_args + variables: + arg1: int + arg2: float + arg3: bool + arg4: string + then: + - logger.log: + format: "YAML service many args: %d, %.2f, %d, %s" + args: [arg1, arg2, arg3, arg4.c_str()] + logger: level: DEBUG diff --git a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml index bf4ef9eafd5..3996d0f169a 100644 --- a/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml +++ b/tests/integration/fixtures/climate_custom_fan_modes_and_presets.yaml @@ -14,6 +14,7 @@ climate: id: test_thermostat name: Test Thermostat Custom Modes sensor: thermostat_sensor + default_preset: "Eco Plus" preset: - name: Away default_target_temperature_low: 16°C diff --git a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h index cdc04d491b4..3dca2da2e9f 100644 --- a/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h +++ b/tests/integration/fixtures/external_components/loop_test_component/loop_test_component.h @@ -39,7 +39,7 @@ template class EnableAction : public Action { public: EnableAction(LoopTestComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->service_enable(); } + void play(const Ts &...x) override { this->parent_->service_enable(); } protected: LoopTestComponent *parent_; @@ -49,7 +49,7 @@ template class DisableAction : public Action { public: DisableAction(LoopTestComponent *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->service_disable(); } + void play(const Ts &...x) override { this->parent_->service_disable(); } protected: LoopTestComponent *parent_; diff --git a/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml new file mode 100644 index 00000000000..836d3f11d5d --- /dev/null +++ b/tests/integration/fixtures/template_alarm_control_panel_many_sensors.yaml @@ -0,0 +1,136 @@ +esphome: + name: template-alarm-many-sensors + friendly_name: "Template Alarm Control Panel with Many Sensors" + +logger: + +host: + +api: + +binary_sensor: + - platform: template + id: sensor1 + name: "Door 1" + - platform: template + id: sensor2 + name: "Door 2" + - platform: template + id: sensor3 + name: "Window 1" + - platform: template + id: sensor4 + name: "Window 2" + - platform: template + id: sensor5 + name: "Motion 1" + - platform: template + id: sensor6 + name: "Motion 2" + - platform: template + id: sensor7 + name: "Glass Break 1" + - platform: template + id: sensor8 + name: "Glass Break 2" + - platform: template + id: sensor9 + name: "Smoke Detector" + - platform: template + id: sensor10 + name: "CO Detector" + +alarm_control_panel: + - platform: template + id: test_alarm + name: "Test Alarm" + codes: + - "1234" + requires_code_to_arm: true + arming_away_time: 5s + arming_home_time: 3s + arming_night_time: 3s + pending_time: 10s + trigger_time: 300s + restore_mode: ALWAYS_DISARMED + binary_sensors: + - input: sensor1 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor2 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: true + chime: true + trigger_mode: DELAYED + - input: sensor3 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor4 + bypass_armed_home: true + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: DELAYED + - input: sensor5 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor6 + bypass_armed_home: false + bypass_armed_night: true + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor7 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor8 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT + - input: sensor9 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + - input: sensor10 + bypass_armed_home: false + bypass_armed_night: false + bypass_auto: false + chime: false + trigger_mode: INSTANT_ALWAYS + on_disarmed: + - logger.log: "Alarm disarmed" + on_arming: + - logger.log: "Alarm arming" + on_armed_away: + - logger.log: "Alarm armed away" + on_armed_home: + - logger.log: "Alarm armed home" + on_armed_night: + - logger.log: "Alarm armed night" + on_pending: + - logger.log: "Alarm pending" + on_triggered: + - logger.log: "Alarm triggered" + on_cleared: + - logger.log: "Alarm cleared" + on_chime: + - logger.log: "Chime activated" + on_ready: + - logger.log: "Sensors ready state changed" diff --git a/tests/integration/fixtures/wait_until_mid_loop_timing.yaml b/tests/integration/fixtures/wait_until_mid_loop_timing.yaml new file mode 100644 index 00000000000..32f59e81a1a --- /dev/null +++ b/tests/integration/fixtures/wait_until_mid_loop_timing.yaml @@ -0,0 +1,109 @@ +# Test for PR #11676 bug: wait_until timeout when triggered mid-component-loop +# This demonstrates that App.get_loop_component_start_time() is stale when +# wait_until is triggered partway through a component's loop execution + +esphome: + name: wait-mid-loop + +host: + +api: + actions: + - action: test_mid_loop_timeout + then: + - logger.log: "=== Test: wait_until triggered mid-loop should timeout correctly ===" + + # Reset test state + - globals.set: + id: test_complete + value: 'false' + + # Trigger the slow script that will call wait_until mid-execution + - script.execute: slow_script + + # Wait for test to complete (should take ~300ms: 100ms delay + 200ms timeout) + - wait_until: + condition: + lambda: return id(test_complete); + timeout: 2s + + - if: + condition: + lambda: return id(test_complete); + then: + - logger.log: "✓ Test PASSED: wait_until timed out correctly" + else: + - logger.log: "✗ Test FAILED: wait_until did not complete properly" + +logger: + level: DEBUG + +globals: + - id: test_complete + type: bool + restore_value: false + initial_value: 'false' + + - id: test_condition + type: bool + restore_value: false + initial_value: 'false' + + - id: timeout_start_time + type: uint32_t + restore_value: false + initial_value: '0' + + - id: timeout_end_time + type: uint32_t + restore_value: false + initial_value: '0' + +script: + # This script simulates a component that takes time during its execution + # When wait_until is triggered mid-script, the loop_component_start_time + # will be stale (from when the script's component loop started) + - id: slow_script + then: + - logger.log: "Script: Starting, about to do some work..." + + # Simulate component doing work for 100ms + # This represents time spent in a component's loop() before triggering wait_until + - delay: 100ms + + - logger.log: "Script: 100ms elapsed, now starting wait_until with 200ms timeout" + - lambda: |- + // Record when timeout starts + id(timeout_start_time) = millis(); + id(test_condition) = false; + + # At this point: + # - Script component's loop started 100ms ago + # - App.loop_component_start_time_ = time from 100ms ago (stale!) + # - wait_until will capture millis() NOW (fresh) + # - BUG: loop() will use stale loop_component_start_time, causing immediate timeout + + - wait_until: + condition: + lambda: return id(test_condition); + timeout: 200ms + + - lambda: |- + // Record when timeout completes + id(timeout_end_time) = millis(); + uint32_t elapsed = id(timeout_end_time) - id(timeout_start_time); + + ESP_LOGD("TEST", "wait_until completed after %u ms (expected ~200ms)", elapsed); + + // Check if timeout took approximately correct time + // Should be ~200ms, not <50ms (immediate timeout) + if (elapsed >= 150 && elapsed <= 250) { + ESP_LOGD("TEST", "✓ Timeout duration correct: %u ms", elapsed); + id(test_complete) = true; + } else { + ESP_LOGE("TEST", "✗ Timeout duration WRONG: %u ms (expected 150-250ms)", elapsed); + if (elapsed < 50) { + ESP_LOGE("TEST", " → Likely BUG: Immediate timeout due to stale loop_component_start_time"); + } + id(test_complete) = false; + } diff --git a/tests/integration/fixtures/wait_until_on_boot.yaml b/tests/integration/fixtures/wait_until_on_boot.yaml new file mode 100644 index 00000000000..358bef971b7 --- /dev/null +++ b/tests/integration/fixtures/wait_until_on_boot.yaml @@ -0,0 +1,47 @@ +# Test for wait_until in on_boot automation +# Reproduces bug where wait_until in on_boot would hang forever +# because WaitUntilAction::setup() would disable_loop() after +# play_complex() had already enabled it. + +esphome: + name: wait-until-on-boot + on_boot: + then: + - logger.log: "on_boot: Starting wait_until test" + - globals.set: + id: on_boot_started + value: 'true' + - wait_until: + condition: + lambda: return id(test_flag); + timeout: 5s + - logger.log: "on_boot: wait_until completed successfully" + +host: + +logger: + level: DEBUG + +globals: + - id: on_boot_started + type: bool + initial_value: 'false' + - id: test_flag + type: bool + initial_value: 'false' + +api: + actions: + - action: set_test_flag + then: + - globals.set: + id: test_flag + value: 'true' + - action: check_on_boot_started + then: + - lambda: |- + if (id(on_boot_started)) { + ESP_LOGI("test", "on_boot has started"); + } else { + ESP_LOGI("test", "on_boot has NOT started"); + } diff --git a/tests/integration/test_api_custom_services.py b/tests/integration/test_api_custom_services.py index 9ae4cdcb5d6..967c5041123 100644 --- a/tests/integration/test_api_custom_services.py +++ b/tests/integration/test_api_custom_services.py @@ -33,12 +33,16 @@ async def test_api_custom_services( # Track log messages yaml_service_future = loop.create_future() + yaml_args_future = loop.create_future() + yaml_many_args_future = loop.create_future() custom_service_future = loop.create_future() custom_args_future = loop.create_future() custom_arrays_future = loop.create_future() # Patterns to match in logs yaml_service_pattern = re.compile(r"YAML service called") + yaml_args_pattern = re.compile(r"YAML service with args: 123, test_value") + yaml_many_args_pattern = re.compile(r"YAML service many args: 42, 3\.14, 1, hello") custom_service_pattern = re.compile(r"Custom test service called!") custom_args_pattern = re.compile( r"Custom service called with: test_string, 456, 1, 78\.90" @@ -51,6 +55,10 @@ async def test_api_custom_services( """Check log output for expected messages.""" if not yaml_service_future.done() and yaml_service_pattern.search(line): yaml_service_future.set_result(True) + elif not yaml_args_future.done() and yaml_args_pattern.search(line): + yaml_args_future.set_result(True) + elif not yaml_many_args_future.done() and yaml_many_args_pattern.search(line): + yaml_many_args_future.set_result(True) elif not custom_service_future.done() and custom_service_pattern.search(line): custom_service_future.set_result(True) elif not custom_args_future.done() and custom_args_pattern.search(line): @@ -71,11 +79,13 @@ async def test_api_custom_services( # List services _, services = await client.list_entities_services() - # Should have 4 services: 1 YAML + 3 CustomAPIDevice - assert len(services) == 4, f"Expected 4 services, found {len(services)}" + # Should have 6 services: 3 YAML + 3 CustomAPIDevice + assert len(services) == 6, f"Expected 6 services, found {len(services)}" # Find our services yaml_service: UserService | None = None + yaml_args_service: UserService | None = None + yaml_many_args_service: UserService | None = None custom_service: UserService | None = None custom_args_service: UserService | None = None custom_arrays_service: UserService | None = None @@ -83,6 +93,10 @@ async def test_api_custom_services( for service in services: if service.name == "test_yaml_service": yaml_service = service + elif service.name == "test_yaml_service_with_args": + yaml_args_service = service + elif service.name == "test_yaml_service_many_args": + yaml_many_args_service = service elif service.name == "custom_test_service": custom_service = service elif service.name == "custom_service_with_args": @@ -91,6 +105,10 @@ async def test_api_custom_services( custom_arrays_service = service assert yaml_service is not None, "test_yaml_service not found" + assert yaml_args_service is not None, "test_yaml_service_with_args not found" + assert yaml_many_args_service is not None, ( + "test_yaml_service_many_args not found" + ) assert custom_service is not None, "custom_test_service not found" assert custom_args_service is not None, "custom_service_with_args not found" assert custom_arrays_service is not None, "custom_service_with_arrays not found" @@ -99,6 +117,44 @@ async def test_api_custom_services( client.execute_service(yaml_service, {}) await asyncio.wait_for(yaml_service_future, timeout=5.0) + # Verify YAML service with args arguments + assert len(yaml_args_service.args) == 2 + yaml_args_types = {arg.name: arg.type for arg in yaml_args_service.args} + assert yaml_args_types["my_int"] == UserServiceArgType.INT + assert yaml_args_types["my_string"] == UserServiceArgType.STRING + + # Test YAML service with arguments + client.execute_service( + yaml_args_service, + { + "my_int": 123, + "my_string": "test_value", + }, + ) + await asyncio.wait_for(yaml_args_future, timeout=5.0) + + # Verify YAML service with many args arguments + assert len(yaml_many_args_service.args) == 4 + yaml_many_args_types = { + arg.name: arg.type for arg in yaml_many_args_service.args + } + assert yaml_many_args_types["arg1"] == UserServiceArgType.INT + assert yaml_many_args_types["arg2"] == UserServiceArgType.FLOAT + assert yaml_many_args_types["arg3"] == UserServiceArgType.BOOL + assert yaml_many_args_types["arg4"] == UserServiceArgType.STRING + + # Test YAML service with many arguments + client.execute_service( + yaml_many_args_service, + { + "arg1": 42, + "arg2": 3.14, + "arg3": True, + "arg4": "hello", + }, + ) + await asyncio.wait_for(yaml_many_args_future, timeout=5.0) + # Test simple CustomAPIDevice service client.execute_service(custom_service, {}) await asyncio.wait_for(custom_service_future, timeout=5.0) diff --git a/tests/integration/test_climate_custom_modes.py b/tests/integration/test_climate_custom_modes.py index ce34959d88f..67a7b0581a4 100644 --- a/tests/integration/test_climate_custom_modes.py +++ b/tests/integration/test_climate_custom_modes.py @@ -2,9 +2,13 @@ from __future__ import annotations -from aioesphomeapi import ClimateInfo, ClimatePreset +import asyncio + +import aioesphomeapi +from aioesphomeapi import ClimateInfo, ClimatePreset, EntityState import pytest +from .state_utils import InitialStateHelper from .types import APIClientConnectedFactory, RunCompiledFunction @@ -14,15 +18,50 @@ async def test_climate_custom_fan_modes_and_presets( run_compiled: RunCompiledFunction, api_client_connected: APIClientConnectedFactory, ) -> None: - """Test that custom presets are properly exposed via API.""" + """Test that custom presets are properly exposed and can be changed.""" + loop = asyncio.get_running_loop() async with run_compiled(yaml_config), api_client_connected() as client: - # Get entities and services + states: dict[int, EntityState] = {} + super_saver_future: asyncio.Future[EntityState] = loop.create_future() + vacation_future: asyncio.Future[EntityState] = loop.create_future() + + def on_state(state: EntityState) -> None: + states[state.key] = state + if isinstance(state, aioesphomeapi.ClimateState): + # Wait for Super Saver preset + if ( + state.custom_preset == "Super Saver" + and state.target_temperature_low == 20.0 + and state.target_temperature_high == 24.0 + and not super_saver_future.done() + ): + super_saver_future.set_result(state) + # Wait for Vacation Mode preset + elif ( + state.custom_preset == "Vacation Mode" + and state.target_temperature_low == 15.0 + and state.target_temperature_high == 18.0 + and not vacation_future.done() + ): + vacation_future.set_result(state) + + # Get entities and set up state synchronization entities, services = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] assert len(climate_infos) == 1, "Expected exactly 1 climate entity" test_climate = climate_infos[0] + # Subscribe with the wrapper that filters initial states + client.subscribe_states(initial_state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states to be broadcast + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + # Verify enum presets are exposed (from preset: config map) assert ClimatePreset.AWAY in test_climate.supported_presets, ( "Expected AWAY in enum presets" @@ -40,3 +79,43 @@ async def test_climate_custom_fan_modes_and_presets( assert "Vacation Mode" in custom_presets, ( "Expected 'Vacation Mode' in custom presets" ) + + # Get initial state and verify default preset + initial_state = initial_state_helper.initial_states.get(test_climate.key) + assert initial_state is not None, "Climate initial state not found" + assert isinstance(initial_state, aioesphomeapi.ClimateState) + assert initial_state.custom_preset == "Eco Plus", ( + f"Expected default preset 'Eco Plus', got '{initial_state.custom_preset}'" + ) + assert initial_state.target_temperature_low == 18.0, ( + f"Expected low temp 18.0, got {initial_state.target_temperature_low}" + ) + assert initial_state.target_temperature_high == 22.0, ( + f"Expected high temp 22.0, got {initial_state.target_temperature_high}" + ) + + # Test changing to "Super Saver" custom preset + client.climate_command(test_climate.key, custom_preset="Super Saver") + + try: + super_saver_state = await asyncio.wait_for(super_saver_future, timeout=5.0) + except TimeoutError: + pytest.fail("Super Saver preset change not received within 5 seconds") + + assert isinstance(super_saver_state, aioesphomeapi.ClimateState) + assert super_saver_state.custom_preset == "Super Saver" + assert super_saver_state.target_temperature_low == 20.0 + assert super_saver_state.target_temperature_high == 24.0 + + # Test changing to "Vacation Mode" custom preset + client.climate_command(test_climate.key, custom_preset="Vacation Mode") + + try: + vacation_state = await asyncio.wait_for(vacation_future, timeout=5.0) + except TimeoutError: + pytest.fail("Vacation Mode preset change not received within 5 seconds") + + assert isinstance(vacation_state, aioesphomeapi.ClimateState) + assert vacation_state.custom_preset == "Vacation Mode" + assert vacation_state.target_temperature_low == 15.0 + assert vacation_state.target_temperature_high == 18.0 diff --git a/tests/integration/test_template_alarm_control_panel_many_sensors.py b/tests/integration/test_template_alarm_control_panel_many_sensors.py new file mode 100644 index 00000000000..856815c731c --- /dev/null +++ b/tests/integration/test_template_alarm_control_panel_many_sensors.py @@ -0,0 +1,118 @@ +"""Integration test for template alarm control panel with many sensors.""" + +from __future__ import annotations + +import aioesphomeapi +from aioesphomeapi.model import APIIntEnum +import pytest + +from .state_utils import InitialStateHelper +from .types import APIClientConnectedFactory, RunCompiledFunction + + +class EspHomeACPFeatures(APIIntEnum): + """ESPHome AlarmControlPanel feature numbers.""" + + ARM_HOME = 1 + ARM_AWAY = 2 + ARM_NIGHT = 4 + TRIGGER = 8 + ARM_CUSTOM_BYPASS = 16 + ARM_VACATION = 32 + + +@pytest.mark.asyncio +async def test_template_alarm_control_panel_many_sensors( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test template alarm control panel with 10 binary sensors using FixedVector.""" + async with run_compiled(yaml_config), api_client_connected() as client: + # Get entity info first + entities, _ = await client.list_entities_services() + + # Find the alarm control panel and binary sensors + alarm_info: aioesphomeapi.AlarmControlPanelInfo | None = None + binary_sensors: list[aioesphomeapi.BinarySensorInfo] = [] + + for entity in entities: + if isinstance(entity, aioesphomeapi.AlarmControlPanelInfo): + alarm_info = entity + elif isinstance(entity, aioesphomeapi.BinarySensorInfo): + binary_sensors.append(entity) + + assert alarm_info is not None, "Alarm control panel entity info not found" + assert alarm_info.name == "Test Alarm" + assert alarm_info.requires_code is True + assert alarm_info.requires_code_to_arm is True + + # Verify we have 10 binary sensors + assert len(binary_sensors) == 10, ( + f"Expected 10 binary sensors, got {len(binary_sensors)}" + ) + + # Verify sensor names + expected_sensor_names = { + "Door 1", + "Door 2", + "Window 1", + "Window 2", + "Motion 1", + "Motion 2", + "Glass Break 1", + "Glass Break 2", + "Smoke Detector", + "CO Detector", + } + actual_sensor_names = {sensor.name for sensor in binary_sensors} + assert actual_sensor_names == expected_sensor_names, ( + f"Sensor names mismatch. Expected: {expected_sensor_names}, " + f"Got: {actual_sensor_names}" + ) + + # Use InitialStateHelper to wait for all initial states + state_helper = InitialStateHelper(entities) + + def on_state(state: aioesphomeapi.EntityState) -> None: + # We'll receive subsequent states here after initial states + pass + + client.subscribe_states(state_helper.on_state_wrapper(on_state)) + + # Wait for all initial states + await state_helper.wait_for_initial_states(timeout=5.0) + + # Verify the alarm state is disarmed initially + alarm_state = state_helper.initial_states.get(alarm_info.key) + assert alarm_state is not None, "Alarm control panel initial state not received" + assert isinstance(alarm_state, aioesphomeapi.AlarmControlPanelEntityState) + assert alarm_state.state == aioesphomeapi.AlarmControlPanelState.DISARMED, ( + f"Expected initial state DISARMED, got {alarm_state.state}" + ) + + # Verify all 10 binary sensors have initial states + binary_sensor_states = [ + state_helper.initial_states.get(sensor.key) for sensor in binary_sensors + ] + assert all(state is not None for state in binary_sensor_states), ( + "Not all binary sensors have initial states" + ) + + # Verify all binary sensor states are BinarySensorState type + for i, state in enumerate(binary_sensor_states): + assert isinstance(state, aioesphomeapi.BinarySensorState), ( + f"Binary sensor {i} state is not BinarySensorState: {type(state)}" + ) + + # Verify supported features + expected_features = ( + EspHomeACPFeatures.ARM_HOME + | EspHomeACPFeatures.ARM_AWAY + | EspHomeACPFeatures.ARM_NIGHT + | EspHomeACPFeatures.TRIGGER + ) + assert alarm_info.supported_features == expected_features, ( + f"Expected supported_features={expected_features} (ARM_HOME|ARM_AWAY|ARM_NIGHT|TRIGGER), " + f"got {alarm_info.supported_features}" + ) diff --git a/tests/integration/test_wait_until_mid_loop_timing.py b/tests/integration/test_wait_until_mid_loop_timing.py new file mode 100644 index 00000000000..01cad747aeb --- /dev/null +++ b/tests/integration/test_wait_until_mid_loop_timing.py @@ -0,0 +1,112 @@ +"""Integration test for PR #11676 mid-loop timing bug. + +This test validates that wait_until timeouts work correctly when triggered +mid-component-loop, where App.get_loop_component_start_time() is stale. + +The bug: When wait_until is triggered partway through a component's loop execution +(e.g., from a script or automation), the cached loop_component_start_time_ is stale +relative to when the action was actually triggered. This causes timeout calculations +to underflow and timeout immediately instead of waiting the specified duration. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_mid_loop_timing( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until timeout works when triggered mid-component-loop. + + This test: + 1. Executes a script that delays 100ms (simulating component work) + 2. Then starts wait_until with 200ms timeout + 3. Verifies timeout takes ~200ms, not <50ms (immediate timeout bug) + """ + loop = asyncio.get_running_loop() + + # Track test results + test_results = { + "timeout_duration": None, + "passed": False, + "failed": False, + "bug_detected": False, + } + + # Patterns for log messages + timeout_duration = re.compile(r"wait_until completed after (\d+) ms") + test_pass = re.compile(r"✓ Timeout duration correct") + test_fail = re.compile(r"✗ Timeout duration WRONG") + bug_pattern = re.compile(r"Likely BUG: Immediate timeout") + test_passed = re.compile(r"✓ Test PASSED") + test_failed = re.compile(r"✗ Test FAILED") + + test_complete = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for test results.""" + # Extract timeout duration + match = timeout_duration.search(line) + if match: + test_results["timeout_duration"] = int(match.group(1)) + + if test_pass.search(line): + test_results["passed"] = True + if test_fail.search(line): + test_results["failed"] = True + if bug_pattern.search(line): + test_results["bug_detected"] = True + + # Final test result + if ( + test_passed.search(line) + or test_failed.search(line) + and not test_complete.done() + ): + test_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get the test service + _, services = await client.list_entities_services() + test_service = next( + (s for s in services if s.name == "test_mid_loop_timeout"), None + ) + assert test_service is not None, "test_mid_loop_timeout service not found" + + # Execute the test + client.execute_service(test_service, {}) + + # Wait for test to complete (100ms delay + 200ms timeout + margins = ~500ms) + await asyncio.wait_for(test_complete, timeout=5.0) + + # Verify results + assert test_results["timeout_duration"] is not None, ( + "Timeout duration not reported" + ) + assert test_results["passed"], ( + f"Test failed: wait_until took {test_results['timeout_duration']}ms, expected ~200ms. " + f"Bug detected: {test_results['bug_detected']}" + ) + assert not test_results["bug_detected"], ( + f"BUG DETECTED: wait_until timed out immediately ({test_results['timeout_duration']}ms) " + "instead of waiting 200ms. This indicates stale loop_component_start_time." + ) + + # Additional validation: timeout should be ~200ms (150-250ms range) + duration = test_results["timeout_duration"] + assert 150 <= duration <= 250, ( + f"Timeout duration {duration}ms outside expected range (150-250ms). " + f"This suggests timing regression from PR #11676." + ) diff --git a/tests/integration/test_wait_until_on_boot.py b/tests/integration/test_wait_until_on_boot.py new file mode 100644 index 00000000000..b42c530c540 --- /dev/null +++ b/tests/integration/test_wait_until_on_boot.py @@ -0,0 +1,91 @@ +"""Integration test for wait_until in on_boot automation. + +This test validates that wait_until works correctly when triggered from on_boot, +which runs at the same setup priority as WaitUntilAction itself. This was broken +before the fix because WaitUntilAction::setup() would unconditionally disable_loop(), +even if play_complex() had already been called and enabled the loop. + +The bug: on_boot fires during StartupTrigger::setup(), which calls WaitUntilAction::play_complex() +before WaitUntilAction::setup() has run. Then when WaitUntilAction::setup() runs, it calls +disable_loop(), undoing the enable_loop() from play_complex(), causing wait_until to hang forever. +""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_wait_until_on_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that wait_until works in on_boot automation with a condition that becomes true later.""" + loop = asyncio.get_running_loop() + + on_boot_started = False + on_boot_completed = False + + on_boot_started_pattern = re.compile(r"on_boot: Starting wait_until test") + on_boot_complete_pattern = re.compile(r"on_boot: wait_until completed successfully") + + on_boot_started_future = loop.create_future() + on_boot_complete_future = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for test progress.""" + nonlocal on_boot_started, on_boot_completed + + if on_boot_started_pattern.search(line): + on_boot_started = True + if not on_boot_started_future.done(): + on_boot_started_future.set_result(True) + + if on_boot_complete_pattern.search(line): + on_boot_completed = True + if not on_boot_complete_future.done(): + on_boot_complete_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Wait for on_boot to start + await asyncio.wait_for(on_boot_started_future, timeout=10.0) + assert on_boot_started, "on_boot did not start" + + # At this point, on_boot is blocked in wait_until waiting for test_flag to become true + # If the bug exists, wait_until's loop is disabled and it will never complete + # even after we set the flag + + # Give a moment for setup to complete + await asyncio.sleep(0.5) + + # Now set the flag that wait_until is waiting for + _, services = await client.list_entities_services() + set_flag_service = next( + (s for s in services if s.name == "set_test_flag"), None + ) + assert set_flag_service is not None, "set_test_flag service not found" + + client.execute_service(set_flag_service, {}) + + # If the fix works, wait_until's loop() will check the condition and proceed + # If the bug exists, wait_until is stuck with disabled loop and will timeout + try: + await asyncio.wait_for(on_boot_complete_future, timeout=2.0) + assert on_boot_completed, ( + "on_boot wait_until did not complete after flag was set" + ) + except TimeoutError: + pytest.fail( + "wait_until in on_boot did not complete within 2s after condition became true. " + "This indicates the bug where WaitUntilAction::setup() disables the loop " + "after play_complex() has already enabled it." + ) diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index e73c1341516..291a23967b2 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -19,6 +19,8 @@ sys.path.insert(0, script_dir) # Import helpers module for patching import helpers # noqa: E402 +import script.helpers # noqa: E402 + spec = importlib.util.spec_from_file_location( "determine_jobs", os.path.join(script_dir, "determine-jobs.py") ) @@ -70,10 +72,18 @@ def mock_changed_files() -> Generator[Mock, None, None]: yield mock +@pytest.fixture +def mock_target_branch_dev() -> Generator[Mock, None, None]: + """Mock get_target_branch to return 'dev' for memory impact tests.""" + with patch.object(determine_jobs, "get_target_branch", return_value="dev") as mock: + yield mock + + @pytest.fixture(autouse=True) -def clear_clang_tidy_cache() -> None: - """Clear the clang-tidy full scan cache before each test.""" +def clear_determine_jobs_caches() -> None: + """Clear all cached functions before each test.""" determine_jobs._is_clang_tidy_full_scan.cache_clear() + determine_jobs._component_has_tests.cache_clear() def test_main_all_tests_should_run( @@ -124,6 +134,16 @@ def test_main_all_tests_should_run( ["wifi", "api"] if not deps else ["wifi", "api", "sensor"] ), ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["wifi", "api", "sensor"]], {}), + ), ): determine_jobs.main() @@ -195,6 +215,16 @@ def test_main_no_tests_should_run( patch.object( determine_jobs, "get_components_with_dependencies", return_value=[] ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([], {}), + ), ): determine_jobs.main() @@ -258,6 +288,16 @@ def test_main_with_branch_argument( patch.object( determine_jobs, "get_components_with_dependencies", return_value=["mqtt"] ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["mqtt"]], {}), + ), ): determine_jobs.main() @@ -543,6 +583,7 @@ def test_main_filters_components_without_tests( with ( patch.object(determine_jobs, "root_path", str(tmp_path)), patch.object(helpers, "root_path", str(tmp_path)), + patch.object(helpers, "create_components_graph", return_value={}), patch("sys.argv", ["determine-jobs.py"]), patch.object( determine_jobs, @@ -562,9 +603,13 @@ def test_main_filters_components_without_tests( ), ), patch.object(determine_jobs, "changed_files", return_value=[]), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), ): # Clear the cache since we're mocking root_path - determine_jobs._component_has_tests.cache_clear() determine_jobs.main() # Check output @@ -640,6 +685,7 @@ def test_main_detects_components_with_variant_tests( with ( patch.object(determine_jobs, "root_path", str(tmp_path)), patch.object(helpers, "root_path", str(tmp_path)), + patch.object(helpers, "create_components_graph", return_value={}), patch("sys.argv", ["determine-jobs.py"]), patch.object( determine_jobs, @@ -661,9 +707,13 @@ def test_main_detects_components_with_variant_tests( ), ), patch.object(determine_jobs, "changed_files", return_value=[]), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), ): # Clear the cache since we're mocking root_path - determine_jobs._component_has_tests.cache_clear() determine_jobs.main() # Check output @@ -687,6 +737,7 @@ def test_main_detects_components_with_variant_tests( # Tests for detect_memory_impact_config function +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> None: """Test memory impact detection when components share a common platform.""" # Create test directory structure @@ -712,7 +763,6 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non "esphome/components/wifi/wifi.cpp", "esphome/components/api/api.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -722,6 +772,7 @@ def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> Non assert result["use_merged_config"] == "true" +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None: """Test memory impact detection with core C++ changes (no component changes).""" # Create test directory structure with fallback component @@ -742,7 +793,6 @@ def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None: "esphome/core/application.cpp", "esphome/core/component.h", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -773,7 +823,6 @@ def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) -> "esphome/config.py", "esphome/core/config.py", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -781,6 +830,7 @@ def test_detect_memory_impact_config_core_python_only_changes(tmp_path: Path) -> assert result["should_run"] == "false" +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None: """Test memory impact detection when components have no common platform.""" # Create test directory structure @@ -806,7 +856,6 @@ def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None: "esphome/components/wifi/wifi.cpp", "esphome/components/logger/logger.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -828,7 +877,6 @@ def test_detect_memory_impact_config_no_changes(tmp_path: Path) -> None: patch.object(determine_jobs, "changed_files") as mock_changed_files, ): mock_changed_files.return_value = [] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -853,13 +901,13 @@ def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) -> mock_changed_files.return_value = [ "esphome/components/my_custom_component/component.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() assert result["should_run"] == "false" +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_includes_base_bus_components( tmp_path: Path, ) -> None: @@ -893,7 +941,6 @@ def test_detect_memory_impact_config_includes_base_bus_components( "esphome/components/uart/automation.h", # Header file with inline code "esphome/components/wifi/wifi.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -903,6 +950,7 @@ def test_detect_memory_impact_config_includes_base_bus_components( assert result["platform"] == "esp32-idf" # Common platform +@pytest.mark.usefixtures("mock_target_branch_dev") def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None: """Test memory impact detection for components with only variant test files. @@ -936,7 +984,6 @@ def test_detect_memory_impact_config_with_variant_tests(tmp_path: Path) -> None: "esphome/components/improv_serial/improv_serial.cpp", "esphome/components/ethernet/ethernet.cpp", ] - determine_jobs._component_has_tests.cache_clear() result = determine_jobs.detect_memory_impact_config() @@ -1119,6 +1166,16 @@ def test_main_core_files_changed_still_detects_components( else ["select", "api", "bluetooth_proxy", "logger"] ), ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + patch.object( + determine_jobs, + "create_intelligent_batches", + return_value=([["select", "api", "bluetooth_proxy", "logger"]], {}), + ), ): determine_jobs.main() @@ -1130,3 +1187,331 @@ def test_main_core_files_changed_still_detects_components( assert "select" in output["changed_components"] assert "api" in output["changed_components"] assert len(output["changed_components"]) > 0 + + +@pytest.mark.usefixtures("mock_target_branch_dev") +def test_detect_memory_impact_config_filters_incompatible_esp32_on_esp8266( + tmp_path: Path, +) -> None: + """Test that ESP32 components are filtered out when ESP8266 platform is selected. + + This test verifies the fix for the issue where ESP32 components were being included + when ESP8266 was selected as the platform, causing build failures in PR 10387. + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # esp32 component only has esp32-idf tests (NOT compatible with esp8266) + esp32_dir = tests_dir / "esp32" + esp32_dir.mkdir(parents=True) + (esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32") + (esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32") + + # esp8266 component only has esp8266-ard test (NOT compatible with esp32) + esp8266_dir = tests_dir / "esp8266" + esp8266_dir.mkdir(parents=True) + (esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266") + + # Mock changed_files to return both esp32 and esp8266 component changes + # Include esp8266-specific filename to trigger esp8266 platform hint + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "tests/components/esp32/common.yaml", + "tests/components/esp8266/test.esp8266-ard.yaml", + "esphome/core/helpers_esp8266.h", # ESP8266-specific file to hint platform + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run + assert result["should_run"] == "true" + + # Platform should be esp8266-ard (due to ESP8266 filename hint) + assert result["platform"] == "esp8266-ard" + + # CRITICAL: Only esp8266 component should be included, not esp32 + # This prevents trying to build ESP32 components on ESP8266 platform + assert result["components"] == ["esp8266"], ( + "When esp8266-ard platform is selected, only esp8266 component should be included, " + "not esp32. This prevents trying to build ESP32 components on ESP8266 platform." + ) + + assert result["use_merged_config"] == "true" + + +@pytest.mark.usefixtures("mock_target_branch_dev") +def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32( + tmp_path: Path, +) -> None: + """Test that ESP8266 components are filtered out when ESP32 platform is selected. + + This is the inverse of the ESP8266 test - ensures filtering works both ways. + """ + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # esp32 component only has esp32-idf tests (NOT compatible with esp8266) + esp32_dir = tests_dir / "esp32" + esp32_dir.mkdir(parents=True) + (esp32_dir / "test.esp32-idf.yaml").write_text("test: esp32") + (esp32_dir / "test.esp32-s3-idf.yaml").write_text("test: esp32") + + # esp8266 component only has esp8266-ard test (NOT compatible with esp32) + esp8266_dir = tests_dir / "esp8266" + esp8266_dir.mkdir(parents=True) + (esp8266_dir / "test.esp8266-ard.yaml").write_text("test: esp8266") + + # Mock changed_files to return both esp32 and esp8266 component changes + # Include MORE esp32-specific filenames to ensure esp32-idf wins the hint count + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "tests/components/esp32/common.yaml", + "tests/components/esp8266/test.esp8266-ard.yaml", + "esphome/components/wifi/wifi_component_esp_idf.cpp", # ESP-IDF hint + "esphome/components/ethernet/ethernet_esp32.cpp", # ESP32 hint + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run + assert result["should_run"] == "true" + + # Platform should be esp32-idf (due to more ESP32-IDF hints) + assert result["platform"] == "esp32-idf" + + # CRITICAL: Only esp32 component should be included, not esp8266 + # This prevents trying to build ESP8266 components on ESP32 platform + assert result["components"] == ["esp32"], ( + "When esp32-idf platform is selected, only esp32 component should be included, " + "not esp8266. This prevents trying to build ESP8266 components on ESP32 platform." + ) + + assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_skips_release_branch(tmp_path: Path) -> None: + """Test that memory impact analysis is skipped for release* branches.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="release"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for release branch + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_skips_beta_branch(tmp_path: Path) -> None: + """Test that memory impact analysis is skipped for beta* branches.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="beta"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for beta branch + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_runs_for_dev_branch(tmp_path: Path) -> None: + """Test that memory impact analysis runs for dev branch.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run for dev branch + assert result["should_run"] == "true" + assert result["components"] == ["wifi"] + + +def test_detect_memory_impact_config_skips_too_many_components( + tmp_path: Path, +) -> None: + """Test that memory impact analysis is skipped when more than 40 components changed.""" + # Create test directory structure with 41 components + tests_dir = tmp_path / "tests" / "components" + component_names = [f"component_{i}" for i in range(41)] + + for component_name in component_names: + comp_dir = tests_dir / component_name + comp_dir.mkdir(parents=True) + (comp_dir / "test.esp32-idf.yaml").write_text(f"test: {component_name}") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = [ + f"esphome/components/{name}/{name}.cpp" for name in component_names + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for too many components (41 > 40) + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) -> None: + """Test that memory impact analysis runs with exactly 40 components (at limit).""" + # Create test directory structure with exactly 40 components + tests_dir = tmp_path / "tests" / "components" + component_names = [f"component_{i}" for i in range(40)] + + for component_name in component_names: + comp_dir = tests_dir / component_name + comp_dir.mkdir(parents=True) + (comp_dir / "test.esp32-idf.yaml").write_text(f"test: {component_name}") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = [ + f"esphome/components/{name}/{name}.cpp" for name in component_names + ] + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run at exactly 40 components (at limit but not over) + assert result["should_run"] == "true" + assert len(result["components"]) == 40 + + +def test_component_batching_beta_branch_40_per_batch( + tmp_path: Path, + mock_should_run_integration_tests: Mock, + mock_should_run_clang_tidy: Mock, + mock_should_run_clang_format: Mock, + mock_should_run_python_linters: Mock, + mock_changed_files: Mock, + mock_determine_cpp_unit_tests: Mock, + capsys: pytest.CaptureFixture[str], +) -> None: + """Test that beta/release branches create batches with 40 actual components each. + + For beta/release branches, all components should be groupable (not isolated), + and each batch should contain 40 actual components with weight 1 each. + This matches the original behavior before consolidation. + """ + # Create 120 test components with test files + component_names = [f"comp_{i:03d}" for i in range(120)] + tests_dir = tmp_path / "tests" / "components" + + for comp in component_names: + comp_dir = tests_dir / comp + comp_dir.mkdir(parents=True) + (comp_dir / "test.esp32-idf.yaml").write_text(f"# Test for {comp}") + + # Setup mocks + mock_should_run_integration_tests.return_value = False + mock_should_run_clang_tidy.return_value = False + mock_should_run_clang_format.return_value = False + mock_should_run_python_linters.return_value = False + mock_determine_cpp_unit_tests.return_value = (False, []) + + # Mock changed_files to return all component files + changed_files = [ + f"esphome/components/{comp}/{comp}.cpp" for comp in component_names + ] + mock_changed_files.return_value = changed_files + + # Run main function with beta branch + # Don't mock create_intelligent_batches - that's what we're testing! + with ( + patch("sys.argv", ["determine-jobs.py", "--branch", "beta"]), + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(script.helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "get_target_branch", return_value="beta"), + patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), + patch.object( + determine_jobs, + "get_changed_components", + return_value=component_names, + ), + patch.object( + determine_jobs, + "filter_component_and_test_files", + side_effect=lambda f: f.startswith("esphome/components/"), + ), + patch.object( + determine_jobs, + "get_components_with_dependencies", + side_effect=lambda files, deps: component_names, + ), + patch.object( + determine_jobs, + "detect_memory_impact_config", + return_value={"should_run": "false"}, + ), + ): + determine_jobs.main() + + # Check output + captured = capsys.readouterr() + output = json.loads(captured.out) + + # Verify batches are present and properly sized + assert "component_test_batches" in output + batches = output["component_test_batches"] + + # Should have 3 batches (120 components / 40 per batch = 3) + assert len(batches) == 3, f"Expected 3 batches, got {len(batches)}" + + # Each batch should have approximately 40 components (all weight=1, groupable) + for i, batch_str in enumerate(batches): + batch_components = batch_str.split() + assert len(batch_components) == 40, ( + f"Batch {i} should have 40 components, got {len(batch_components)}" + ) + + # Verify all 120 components are in batches + all_components = [] + for batch_str in batches: + all_components.extend(batch_str.split()) + assert len(all_components) == 120 + assert set(all_components) == set(component_names) diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 1046512a141..c51273f2980 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1,5 +1,6 @@ """Unit tests for script/helpers.py module.""" +from collections.abc import Generator import json import os from pathlib import Path @@ -30,6 +31,13 @@ print_file_list = helpers.print_file_list get_all_dependencies = helpers.get_all_dependencies +@pytest.fixture(autouse=True) +def clear_helpers_cache() -> None: + """Clear cached functions before each test.""" + helpers._get_github_event_data.cache_clear() + helpers._get_changed_files_github_actions.cache_clear() + + @pytest.mark.parametrize( ("github_ref", "expected_pr_number"), [ @@ -1093,6 +1101,11 @@ def test_parse_list_components_output(output: str, expected: list[str]) -> None: ("tests/components/", None), # No component name ("esphome/components", None), # No trailing slash ("tests/components", None), # No trailing slash + # Files in component directories that are not components + ("tests/components/.gitignore", None), # Hidden file + ("tests/components/README.md", None), # Documentation file + ("esphome/components/__init__.py", None), # Python init file + ("tests/components/main.cpp", None), # File with extension ], ) def test_get_component_from_path( @@ -1101,3 +1114,262 @@ def test_get_component_from_path( """Test extraction of component names from file paths.""" result = helpers.get_component_from_path(file_path) assert result == expected_component + + +# Components graph cache tests + + +@pytest.fixture +def mock_git_output() -> str: + """Fixture for mock git ls-files output with realistic component files. + + Includes examples of AUTO_LOAD in sensor.py and binary_sensor.py files, + which is why we need to hash all .py files, not just __init__.py. + """ + return ( + "100644 abc123... 0 esphome/components/wifi/__init__.py\n" + "100644 def456... 0 esphome/components/api/__init__.py\n" + "100644 ghi789... 0 esphome/components/xiaomi_lywsd03mmc/__init__.py\n" + "100644 jkl012... 0 esphome/components/xiaomi_lywsd03mmc/sensor.py\n" + "100644 mno345... 0 esphome/components/xiaomi_cgpr1/__init__.py\n" + "100644 pqr678... 0 esphome/components/xiaomi_cgpr1/binary_sensor.py\n" + ) + + +@pytest.fixture +def mock_cache_file(tmp_path: Path) -> Path: + """Fixture for a temporary cache file path.""" + return tmp_path / "components_graph.json" + + +@pytest.fixture(autouse=True) +def clear_cache_key_cache() -> None: + """Clear the components graph cache key cache before each test.""" + helpers.get_components_graph_cache_key.cache_clear() + + +@pytest.fixture +def mock_subprocess_run() -> Generator[Mock, None, None]: + """Fixture to mock subprocess.run for git commands.""" + with patch("subprocess.run") as mock_run: + yield mock_run + + +def test_cache_key_generation(mock_git_output: str, mock_subprocess_run: Mock) -> None: + """Test that cache key is generated based on git file hashes.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + key = helpers.get_components_graph_cache_key() + + # Should be a 64-character hex string (SHA256) + assert len(key) == 64 + assert all(c in "0123456789abcdef" for c in key) + + +def test_cache_key_consistent_for_same_files( + mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that same git output produces same cache key.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + key1 = helpers.get_components_graph_cache_key() + key2 = helpers.get_components_graph_cache_key() + + assert key1 == key2 + + +def test_cache_key_different_for_changed_files(mock_subprocess_run: Mock) -> None: + """Test that different git output produces different cache key. + + This test demonstrates that changes to any .py file (not just __init__.py) + will invalidate the cache, which is important because AUTO_LOAD can be + defined in sensor.py, binary_sensor.py, etc. + """ + mock_result1 = Mock() + mock_result1.stdout = ( + "100644 abc123... 0 esphome/components/xiaomi_lywsd03mmc/sensor.py\n" + ) + + mock_result2 = Mock() + # Same file, different hash - simulates a change to AUTO_LOAD + mock_result2.stdout = ( + "100644 xyz789... 0 esphome/components/xiaomi_lywsd03mmc/sensor.py\n" + ) + + mock_subprocess_run.return_value = mock_result1 + key1 = helpers.get_components_graph_cache_key() + + helpers.get_components_graph_cache_key.cache_clear() + mock_subprocess_run.return_value = mock_result2 + key2 = helpers.get_components_graph_cache_key() + + assert key1 != key2 + + +def test_cache_key_uses_git_ls_files( + mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that git ls-files command is called correctly.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + helpers.get_components_graph_cache_key() + + # Verify git ls-files was called with correct arguments + mock_subprocess_run.assert_called_once() + call_args = mock_subprocess_run.call_args + assert call_args[0][0] == [ + "git", + "ls-files", + "-s", + "esphome/components/**/*.py", + ] + assert call_args[1]["capture_output"] is True + assert call_args[1]["text"] is True + assert call_args[1]["check"] is True + assert call_args[1]["close_fds"] is False + + +def test_cache_hit_returns_cached_graph( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache hit returns cached data without rebuilding.""" + mock_graph = {"wifi": ["network"], "api": ["socket"]} + cache_key = "a" * 64 + cache_data = { + "_version": helpers.COMPONENTS_GRAPH_CACHE_VERSION, + "_cache_key": cache_key, + "graph": mock_graph, + } + + # Write cache file + cache_file = tmp_path / "components_graph.json" + cache_file.write_text(json.dumps(cache_data)) + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + with ( + patch("helpers.get_components_graph_cache_key", return_value=cache_key), + patch("helpers.temp_folder", str(tmp_path)), + ): + result = helpers.create_components_graph() + assert result == mock_graph + + +def test_cache_miss_no_cache_file( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when no cache file exists.""" + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="test_key"), + ): + result = helpers.create_components_graph() + # Should return empty graph for empty components directory + assert result == {} + + +def test_cache_miss_version_mismatch( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when version doesn't match.""" + cache_data = { + "_version": 999, # Wrong version + "_cache_key": "test_key", + "graph": {"old": ["data"]}, + } + + cache_file = tmp_path / ".temp" / "components_graph.json" + cache_file.parent.mkdir(parents=True) + cache_file.write_text(json.dumps(cache_data)) + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="test_key"), + ): + result = helpers.create_components_graph() + # Should rebuild and return empty graph, not use cached data + assert result == {} + + +def test_cache_miss_key_mismatch( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when cache key doesn't match.""" + cache_data = { + "_version": helpers.COMPONENTS_GRAPH_CACHE_VERSION, + "_cache_key": "old_key", + "graph": {"old": ["data"]}, + } + + cache_file = tmp_path / ".temp" / "components_graph.json" + cache_file.parent.mkdir(parents=True) + cache_file.write_text(json.dumps(cache_data)) + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="new_key"), + ): + result = helpers.create_components_graph() + # Should rebuild and return empty graph, not use cached data with old key + assert result == {} + + +def test_cache_miss_corrupted_json( + tmp_path: Path, mock_git_output: str, mock_subprocess_run: Mock +) -> None: + """Test that cache miss rebuilds graph when cache file has invalid JSON.""" + cache_file = tmp_path / ".temp" / "components_graph.json" + cache_file.parent.mkdir(parents=True) + cache_file.write_text("{invalid json") + + mock_result = Mock() + mock_result.stdout = mock_git_output + mock_subprocess_run.return_value = mock_result + + # Create minimal components directory structure + components_dir = tmp_path / "esphome" / "components" + components_dir.mkdir(parents=True) + + with ( + patch("helpers.root_path", str(tmp_path)), + patch("helpers.temp_folder", str(tmp_path / ".temp")), + patch("helpers.get_components_graph_cache_key", return_value="test_key"), + ): + result = helpers.create_components_graph() + # Should handle corruption gracefully and rebuild + assert result == {} diff --git a/tests/test_build_components/common/i2c/nrf52.yaml b/tests/test_build_components/common/i2c/nrf52.yaml new file mode 100644 index 00000000000..b86cdf0d693 --- /dev/null +++ b/tests/test_build_components/common/i2c/nrf52.yaml @@ -0,0 +1,11 @@ +# Common I2C configuration for NRF52 tests + +substitutions: + scl_pin: P0.04 + sda_pin: P0.05 + +i2c: + - id: i2c_bus + scl: ${scl_pin} + sda: ${sda_pin} + scan: true diff --git a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml index a6e7c264cbe..443ebbebd9f 100644 --- a/tests/test_build_components/common/i2c_camera/esp32-idf.yaml +++ b/tests/test_build_components/common/i2c_camera/esp32-idf.yaml @@ -1,4 +1,6 @@ # I2C bus for camera sensor +psram: + i2c: - id: i2c_camera_bus sda: 25 diff --git a/tests/test_build_components/common/remote_receiver/esp32-ard.yaml b/tests/test_build_components/common/remote_receiver/esp32-ard.yaml new file mode 100644 index 00000000000..af5c2f24097 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common remote_receiver configuration for ESP32 Arduino tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO32 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% diff --git a/tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml b/tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml new file mode 100644 index 00000000000..26b288b4274 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common remote_receiver configuration for ESP32-C3 Arduino tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO10 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 378a226dc20..e52cb248319 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -671,44 +671,50 @@ class TestEsphomeCore: os.environ.pop("ESPHOME_DATA_DIR", None) assert target.data_dir == Path(expected_default) - def test_platformio_cache_dir_with_env_var(self): - """Test platformio_cache_dir when PLATFORMIO_CACHE_DIR env var is set.""" - target = core.EsphomeCore() - test_cache_dir = "/custom/cache/dir" + def test_web_port__none(self, target): + """Test web_port returns None when web_server is not configured.""" + target.config = {} + assert target.web_port is None - with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": test_cache_dir}): - assert target.platformio_cache_dir == test_cache_dir + def test_web_port__explicit_web_server_default_port(self, target): + """Test web_port returns 80 when web_server is explicitly configured without port.""" + target.config = {const.CONF_WEB_SERVER: {}} + assert target.web_port == 80 - def test_platformio_cache_dir_without_env_var(self): - """Test platformio_cache_dir defaults to ~/.platformio/.cache.""" - target = core.EsphomeCore() + def test_web_port__explicit_web_server_custom_port(self, target): + """Test web_port returns custom port when web_server is configured with port.""" + target.config = {const.CONF_WEB_SERVER: {const.CONF_PORT: 8080}} + assert target.web_port == 8080 - with patch.dict(os.environ, {}, clear=True): - # Ensure env var is not set - os.environ.pop("PLATFORMIO_CACHE_DIR", None) - expected = os.path.expanduser("~/.platformio/.cache") - assert target.platformio_cache_dir == expected + def test_web_port__ota_web_server_platform_only(self, target): + """ + Test web_port returns None when ota.web_server platform is explicitly configured. - def test_platformio_cache_dir_empty_env_var(self): - """Test platformio_cache_dir with empty env var falls back to default.""" - target = core.EsphomeCore() + This is a critical test for Dashboard Issue #766: + https://github.com/esphome/dashboard/issues/766 - with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": ""}): - expected = os.path.expanduser("~/.platformio/.cache") - assert target.platformio_cache_dir == expected + When ota: platform: web_server is explicitly configured (or auto-loaded by captive_portal): + - "web_server" appears in loaded_integrations (platform name added to integrations) + - "ota/web_server" appears in loaded_platforms + - But CONF_WEB_SERVER is NOT in config (only the platform is loaded, not the component) + - web_port MUST return None (no web UI available) + - Dashboard should NOT show VISIT button - def test_platformio_cache_dir_whitespace_env_var(self): - """Test platformio_cache_dir with whitespace-only env var falls back to default.""" - target = core.EsphomeCore() - - with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": " "}): - expected = os.path.expanduser("~/.platformio/.cache") - assert target.platformio_cache_dir == expected - - def test_platformio_cache_dir_docker_addon_path(self): - """Test platformio_cache_dir in Docker/HA addon environment.""" - target = core.EsphomeCore() - addon_cache = "/data/cache/platformio" - - with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": addon_cache}): - assert target.platformio_cache_dir == addon_cache + This test ensures web_port only checks CONF_WEB_SERVER in config, not loaded_integrations. + """ + # Simulate config with ota.web_server platform but no web_server component + # This happens when: + # 1. User explicitly configures: ota: - platform: web_server + # 2. OR captive_portal auto-loads ota.web_server + target.config = { + const.CONF_OTA: [ + { + "platform": "web_server", + # OTA web_server platform config would be here + } + ], + # Note: CONF_WEB_SERVER is NOT in config - only the OTA platform + } + # Even though "web_server" is in loaded_integrations due to the platform, + # web_port must return None because the full web_server component is not configured + assert target.web_port is None diff --git a/tests/unit_tests/test_lambda_dedup.py b/tests/unit_tests/test_lambda_dedup.py new file mode 100644 index 00000000000..ebc217f308d --- /dev/null +++ b/tests/unit_tests/test_lambda_dedup.py @@ -0,0 +1,194 @@ +"""Tests for lambda deduplication in cpp_generator.""" + +from esphome import cpp_generator as cg +from esphome.core import CORE + + +def test_deduplicate_identical_lambdas() -> None: + """Test that identical stateless lambdas are deduplicated.""" + # Create two identical lambda expressions + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + # Try to deduplicate them + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + # Both should get the same function name (deduplication happened) + assert func_name1 == func_name2 + assert func_name1 == "shared_lambda_0" + + +def test_different_lambdas_not_deduplicated() -> None: + """Test that different lambdas get different function names.""" + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 24;"], # Different content + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + # Different lambdas should get different function names + assert func_name1 != func_name2 + assert func_name1 == "shared_lambda_0" + assert func_name2 == "shared_lambda_1" + + +def test_different_return_types_not_deduplicated() -> None: + """Test that lambdas with different return types are not deduplicated.""" + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return 42;"], # Same content + parameters=[], + capture="", + return_type=cg.RawExpression("float"), # Different return type + ) + + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + # Different return types = different functions + assert func_name1 != func_name2 + + +def test_different_parameters_not_deduplicated() -> None: + """Test that lambdas with different parameters are not deduplicated.""" + lambda1 = cg.LambdaExpression( + parts=["return x;"], + parameters=[("int", "x")], + capture="", + return_type=cg.RawExpression("int"), + ) + + lambda2 = cg.LambdaExpression( + parts=["return x;"], # Same content + parameters=[("float", "x")], # Different parameter type + capture="", + return_type=cg.RawExpression("int"), + ) + + func_name1 = cg._get_shared_lambda_name(lambda1) + func_name2 = cg._get_shared_lambda_name(lambda2) + + # Different parameters = different functions + assert func_name1 != func_name2 + + +def test_flush_lambda_dedup_declarations() -> None: + """Test that deferred declarations are properly stored for later flushing.""" + # Create a lambda which will create a deferred declaration + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + + cg._get_shared_lambda_name(lambda1) + + # Check that declaration was stored + assert cg._KEY_LAMBDA_DEDUP_DECLARATIONS in CORE.data + assert len(CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS]) == 1 + + # Verify the declaration content is correct + declaration = CORE.data[cg._KEY_LAMBDA_DEDUP_DECLARATIONS][0] + assert "shared_lambda_0" in declaration + assert "return 42;" in declaration + + # Note: The actual flushing happens via CORE.add_job with FINAL priority + # during real code generation, so we don't test that here + + +def test_shared_function_lambda_expression() -> None: + """Test SharedFunctionLambdaExpression behaves correctly.""" + shared_lambda = cg.SharedFunctionLambdaExpression( + func_name="shared_lambda_0", + parameters=[], + return_type=cg.RawExpression("int"), + ) + + # Should output just the function name + assert str(shared_lambda) == "shared_lambda_0" + + # Should have empty capture (stateless) + assert shared_lambda.capture == "" + + # Should have empty content (just a reference) + assert shared_lambda.content == "" + + +def test_lambda_deduplication_counter() -> None: + """Test that lambda counter increments correctly.""" + # Create 3 different lambdas + for i in range(3): + lambda_expr = cg.LambdaExpression( + parts=[f"return {i};"], + parameters=[], + capture="", + return_type=cg.RawExpression("int"), + ) + func_name = cg._get_shared_lambda_name(lambda_expr) + assert func_name == f"shared_lambda_{i}" + + +def test_lambda_format_body() -> None: + """Test that format_body correctly formats lambda body with source.""" + # Without source + lambda1 = cg.LambdaExpression( + parts=["return 42;"], + parameters=[], + capture="", + return_type=None, + source=None, + ) + assert lambda1.format_body() == "return 42;" + + # With source would need a proper source object, skip for now + + +def test_stateful_lambdas_not_deduplicated() -> None: + """Test that stateful lambdas (non-empty capture) are not deduplicated.""" + # _get_shared_lambda_name is only called for stateless lambdas (capture == "") + # Stateful lambdas bypass deduplication entirely in process_lambda + + # Verify that a stateful lambda would NOT get deduplicated + # by checking it's not in the stateless dedup cache + stateful_lambda = cg.LambdaExpression( + parts=["return x + y;"], + parameters=[], + capture="=", # Non-empty capture means stateful + return_type=cg.RawExpression("int"), + ) + + # Stateful lambdas should NOT be passed to _get_shared_lambda_name + # This is enforced by the `if capture == ""` check in process_lambda + # We verify the lambda has a non-empty capture + assert stateful_lambda.capture != "" + assert stateful_lambda.capture == "=" diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 9e5f3993815..ccbc5a1306f 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1166,6 +1166,56 @@ def test_upload_program_ota_with_mqtt_resolution( ) +def test_upload_program_ota_with_mqtt_empty_broker( + mock_mqtt_get_ip: Mock, + mock_is_ip_address: Mock, + mock_run_ota: Mock, + tmp_path: Path, + caplog: CaptureFixture, +) -> None: + """Test upload_program with OTA when MQTT broker is empty (issue #11653).""" + setup_core(address="192.168.1.50", platform=PLATFORM_ESP32, tmp_path=tmp_path) + + mock_is_ip_address.return_value = True + mock_mqtt_get_ip.side_effect = EsphomeError( + "Cannot discover IP via MQTT as the broker is not configured" + ) + mock_run_ota.return_value = (0, "192.168.1.50") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + } + ], + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_MDNS: { + CONF_DISABLED: True, + }, + } + args = MockArgs(username="user", password="pass", client_id="client") + devices = ["MQTTIP", "192.168.1.50"] + + exit_code, host = upload_program(config, args, devices) + + assert exit_code == 0 + assert host == "192.168.1.50" + # Verify MQTT was attempted but failed gracefully + mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client") + # Verify we fell back to the IP address + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.50"], 3232, None, expected_firmware + ) + # Verify warning was logged + assert "MQTT IP discovery failed" in caplog.text + + @patch("esphome.__main__.importlib.import_module") def test_upload_program_platform_specific_handler( mock_import: Mock, diff --git a/tests/unit_tests/test_mqtt.py b/tests/unit_tests/test_mqtt.py new file mode 100644 index 00000000000..4c2c34dff12 --- /dev/null +++ b/tests/unit_tests/test_mqtt.py @@ -0,0 +1,91 @@ +"""Unit tests for esphome.mqtt module.""" + +from __future__ import annotations + +import pytest + +from esphome.const import CONF_BROKER, CONF_ESPHOME, CONF_MQTT, CONF_NAME +from esphome.core import EsphomeError +from esphome.mqtt import get_esphome_device_ip + + +def test_get_esphome_device_ip_empty_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is empty.""" + config = { + CONF_MQTT: { + CONF_BROKER: "", + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_none_broker() -> None: + """Test that get_esphome_device_ip raises EsphomeError when broker is None.""" + config = { + CONF_MQTT: { + CONF_BROKER: None, + }, + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the broker is not configured", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_mqtt() -> None: + """Test that get_esphome_device_ip raises EsphomeError when mqtt config is missing.""" + config = { + CONF_ESPHOME: { + CONF_NAME: "test-device", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the mqtt:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_esphome() -> None: + """Test that get_esphome_device_ip raises EsphomeError when esphome config is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config) + + +def test_get_esphome_device_ip_missing_name() -> None: + """Test that get_esphome_device_ip raises EsphomeError when device name is missing.""" + config = { + CONF_MQTT: { + CONF_BROKER: "mqtt.local", + }, + CONF_ESPHOME: {}, + } + + with pytest.raises( + EsphomeError, + match="Cannot discover IP via MQTT as the config does not include the device name:", + ): + get_esphome_device_ip(config)