diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index d86c5d6dab2..166d3fd70da 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -1,10 +1,12 @@ from dataclasses import dataclass +from esphome import automation import esphome.codegen as cg from esphome.components import esp32, network, psram, socket, wifi import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM -from esphome.core import CORE +from esphome.core import CORE, ID +from esphome.cpp_generator import TemplateArgsType from esphome.types import ConfigType # mdns for autodiscovery @@ -22,6 +24,13 @@ SendspinHub = sendspin_ns.class_( ) +SendspinSwitchCommandAction = sendspin_ns.class_( + "SendspinSwitchCommandAction", + automation.Action, + cg.Parented.template(SendspinHub), +) + + @dataclass class SendspinConfiguration: artwork_support: bool = False @@ -101,6 +110,41 @@ CONFIG_SCHEMA = cv.All( ) +def _request_controller_role(config: ConfigType) -> ConfigType: + """Request the controller role for the sendspin.switch action.""" + request_controller_support() + return config + + +SENDSPIN_SIMPLE_ACTION_SCHEMA = cv.All( + automation.maybe_simple_id( + cv.Schema( + { + cv.GenerateID(): cv.use_id(SendspinHub), + } + ) + ), + _request_controller_role, +) + + +@automation.register_action( + "sendspin.switch", + SendspinSwitchCommandAction, + SENDSPIN_SIMPLE_ACTION_SCHEMA, + synchronous=True, +) +async def sendspin_switch_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/sendspin/automation.h b/esphome/components/sendspin/automation.h new file mode 100644 index 00000000000..be3b1eb39d3 --- /dev/null +++ b/esphome/components/sendspin/automation.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/core/automation.h" +#include "sendspin_hub.h" + +namespace esphome::sendspin_ { + +#ifdef USE_SENDSPIN_CONTROLLER +template class SendspinSwitchCommandAction : public Action, public Parented { + public: + void play(const Ts &...x) override { + // Clear any EXTERNAL_SOURCE state so the switch command is followed + this->parent_->update_state(sendspin::SendspinClientState::SYNCHRONIZED); + this->parent_->send_client_command(sendspin::SendspinControllerCommand::SWITCH); + } +}; +#endif // USE_SENDSPIN_CONTROLLER + +} // namespace esphome::sendspin_ + +#endif // USE_ESP32 diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 94338887946..ec419f77412 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -31,6 +31,11 @@ void SendspinHub::setup() { this->client_->set_network_provider(this); this->client_->set_persistence_provider(this); +#ifdef USE_SENDSPIN_CONTROLLER + this->controller_role_ = &this->client_->add_controller(); + this->controller_role_->set_listener(this); +#endif + if (!this->client_->start_server()) { ESP_LOGE(TAG, "Failed to start Sendspin server"); this->mark_failed(); @@ -138,6 +143,23 @@ std::optional SendspinHub::load_last_server_hash() { return std::nullopt; } +// --- Sendspin role specific methods/overrides --- + +#ifdef USE_SENDSPIN_CONTROLLER +// THREAD CONTEXT: Main loop (invoked from ESPHome actions / other components) +void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional volume, + std::optional mute) { + if (this->is_ready()) { + this->controller_role_->send_command(command, volume, mute); + } +} + +// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop()) +void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) { + this->controller_state_callbacks_.call(state); +} +#endif + } // namespace esphome::sendspin_ #endif // USE_ESP32 diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index 4402d25fbd1..1e217e0ea2e 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -13,6 +13,10 @@ #include #include +#ifdef USE_SENDSPIN_CONTROLLER +#include +#endif + #include #include #include @@ -50,6 +54,9 @@ struct LastPlayedServerPref { /// (for services the library pulls; e.g., persistence, network readiness). /// - User -> library communication uses exposed functions on the client and role objects that the user calls. class SendspinHub final : public Component, +#ifdef USE_SENDSPIN_CONTROLLER + public sendspin::ControllerRoleListener, +#endif public sendspin::SendspinClientListener, public sendspin::SendspinNetworkProvider, public sendspin::SendspinPersistenceProvider { @@ -94,6 +101,17 @@ class SendspinHub final : public Component, void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + // --- Sendspin role specific methods --- + +#ifdef USE_SENDSPIN_CONTROLLER + void send_client_command(sendspin::SendspinControllerCommand command, std::optional volume = std::nullopt, + std::optional mute = std::nullopt); + + template void add_controller_state_callback(F &&callback) { + this->controller_state_callbacks_.add(std::forward(callback)); + } +#endif + protected: /// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info. sendspin::SendspinClientConfig build_client_config_(); @@ -112,6 +130,19 @@ class SendspinHub final : public Component, bool save_last_server_hash(uint32_t hash) override; std::optional load_last_server_hash() override; + // --- Sendspin role specific methods/overrides/member variables --- + +#ifdef USE_SENDSPIN_CONTROLLER + sendspin::ControllerRole *controller_role_{nullptr}; + + void on_controller_state(const sendspin::ServerStateControllerObject &state) override; + + // Callback fan-out to child components; they filter as needed + CallbackManager controller_state_callbacks_{}; +#endif + + // --- Core member variables --- + ESPPreferenceObject last_played_server_pref_; std::unique_ptr client_; diff --git a/tests/components/sendspin/common-action.yaml b/tests/components/sendspin/common-action.yaml new file mode 100644 index 00000000000..16f19ad7d17 --- /dev/null +++ b/tests/components/sendspin/common-action.yaml @@ -0,0 +1,8 @@ +# `sendspin.switch` action enables the controller role, so we use a standalone test +packages: + base: !include common.yaml + +wifi: + on_connect: + then: + - sendspin.switch: diff --git a/tests/components/sendspin/test-action.esp32-idf.yaml b/tests/components/sendspin/test-action.esp32-idf.yaml new file mode 100644 index 00000000000..70a7ee1bade --- /dev/null +++ b/tests/components/sendspin/test-action.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-action.yaml