[sendspin] Add controller role and sendspin.switch action (PR2) (#15929)

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Kevin Ahrendt
2026-04-23 21:22:47 -04:00
committed by GitHub
parent ddf1426f86
commit b4a86e46b2
6 changed files with 132 additions and 1 deletions
+45 -1
View File
@@ -1,10 +1,12 @@
from dataclasses import dataclass from dataclasses import dataclass
from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, network, psram, socket, wifi from esphome.components import esp32, network, psram, socket, wifi
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM 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 from esphome.types import ConfigType
# mdns for autodiscovery # mdns for autodiscovery
@@ -22,6 +24,13 @@ SendspinHub = sendspin_ns.class_(
) )
SendspinSwitchCommandAction = sendspin_ns.class_(
"SendspinSwitchCommandAction",
automation.Action,
cg.Parented.template(SendspinHub),
)
@dataclass @dataclass
class SendspinConfiguration: class SendspinConfiguration:
artwork_support: bool = False 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: async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
+25
View File
@@ -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<typename... Ts> class SendspinSwitchCommandAction : public Action<Ts...>, public Parented<SendspinHub> {
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
@@ -31,6 +31,11 @@ void SendspinHub::setup() {
this->client_->set_network_provider(this); this->client_->set_network_provider(this);
this->client_->set_persistence_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()) { if (!this->client_->start_server()) {
ESP_LOGE(TAG, "Failed to start Sendspin server"); ESP_LOGE(TAG, "Failed to start Sendspin server");
this->mark_failed(); this->mark_failed();
@@ -138,6 +143,23 @@ std::optional<uint32_t> SendspinHub::load_last_server_hash() {
return std::nullopt; 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<uint8_t> volume,
std::optional<bool> 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_ } // namespace esphome::sendspin_
#endif // USE_ESP32 #endif // USE_ESP32
@@ -13,6 +13,10 @@
#include <sendspin/config.h> #include <sendspin/config.h>
#include <sendspin/types.h> #include <sendspin/types.h>
#ifdef USE_SENDSPIN_CONTROLLER
#include <sendspin/controller_role.h>
#endif
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <optional> #include <optional>
@@ -50,6 +54,9 @@ struct LastPlayedServerPref {
/// (for services the library pulls; e.g., persistence, network readiness). /// (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. /// - User -> library communication uses exposed functions on the client and role objects that the user calls.
class SendspinHub final : public Component, class SendspinHub final : public Component,
#ifdef USE_SENDSPIN_CONTROLLER
public sendspin::ControllerRoleListener,
#endif
public sendspin::SendspinClientListener, public sendspin::SendspinClientListener,
public sendspin::SendspinNetworkProvider, public sendspin::SendspinNetworkProvider,
public sendspin::SendspinPersistenceProvider { 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; } 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<uint8_t> volume = std::nullopt,
std::optional<bool> mute = std::nullopt);
template<typename F> void add_controller_state_callback(F &&callback) {
this->controller_state_callbacks_.add(std::forward<F>(callback));
}
#endif
protected: protected:
/// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info. /// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info.
sendspin::SendspinClientConfig build_client_config_(); sendspin::SendspinClientConfig build_client_config_();
@@ -112,6 +130,19 @@ class SendspinHub final : public Component,
bool save_last_server_hash(uint32_t hash) override; bool save_last_server_hash(uint32_t hash) override;
std::optional<uint32_t> load_last_server_hash() override; std::optional<uint32_t> 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<void(const sendspin::ServerStateControllerObject &)> controller_state_callbacks_{};
#endif
// --- Core member variables ---
ESPPreferenceObject last_played_server_pref_; ESPPreferenceObject last_played_server_pref_;
std::unique_ptr<sendspin::SendspinClient> client_; std::unique_ptr<sendspin::SendspinClient> client_;
@@ -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:
@@ -0,0 +1 @@
<<: !include common-action.yaml