mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 18:18:43 +00:00
[sendspin] Add a switch platform to enable and disable the client (#19361)
This commit is contained in:
@@ -478,6 +478,7 @@ esphome/components/sendspin/image/* @kahrendt
|
||||
esphome/components/sendspin/media_player/* @kahrendt
|
||||
esphome/components/sendspin/media_source/* @kahrendt
|
||||
esphome/components/sendspin/sensor/* @kahrendt
|
||||
esphome/components/sendspin/switch/* @kahrendt
|
||||
esphome/components/sendspin/text_sensor/* @kahrendt
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
|
||||
@@ -97,6 +97,10 @@ void SendspinMediaPlayer::control(const media_player::MediaPlayerCall &call) {
|
||||
// Ignore any commands sent before the media player is setup
|
||||
return;
|
||||
}
|
||||
if (!this->parent_->is_client_running()) {
|
||||
ESP_LOGW(TAG, "Cannot control media player: Sendspin is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
auto volume = call.get_volume();
|
||||
if (volume.has_value()) {
|
||||
|
||||
@@ -45,6 +45,8 @@ bool SendspinMediaSource::can_handle(const std::string &uri) const { return uri.
|
||||
|
||||
// THREAD CONTEXT: Main loop (media_source.h documents play_uri as main-loop only)
|
||||
bool SendspinMediaSource::play_uri(const std::string &uri) {
|
||||
// The queued request has been delivered, whatever the outcome, so the next stream start may request again
|
||||
this->pending_start_ = false;
|
||||
if (!this->is_ready() || this->is_failed() || !this->has_listener()) {
|
||||
return false;
|
||||
}
|
||||
@@ -54,6 +56,11 @@ bool SendspinMediaSource::play_uri(const std::string &uri) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->parent_->is_client_running()) {
|
||||
ESP_LOGE(TAG, "Cannot play '%s': Sendspin is disabled", uri.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uri.starts_with(URI_PREFIX)) {
|
||||
ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
|
||||
return false;
|
||||
@@ -74,7 +81,6 @@ bool SendspinMediaSource::play_uri(const std::string &uri) {
|
||||
}
|
||||
|
||||
// Tell the orchestrator we're now playing so it routes audio output from us
|
||||
this->pending_start_ = false;
|
||||
this->set_state_(media_source::MediaSourceState::PLAYING);
|
||||
|
||||
return true;
|
||||
@@ -82,6 +88,15 @@ bool SendspinMediaSource::play_uri(const std::string &uri) {
|
||||
|
||||
// THREAD CONTEXT: Main loop (media_source.h documents handle_command as main-loop only)
|
||||
void SendspinMediaSource::handle_command(media_source::MediaSourceCommand command) {
|
||||
if (!this->parent_->is_client_running()) {
|
||||
if (command == media_source::MediaSourceCommand::STOP) {
|
||||
// Nothing is playing, so the orchestrator gets its pipeline back straight away
|
||||
this->on_stream_end();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Cannot handle command: Sendspin is disabled");
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (command) {
|
||||
case media_source::MediaSourceCommand::STOP: {
|
||||
if (!this->pending_start_) {
|
||||
|
||||
@@ -21,10 +21,6 @@ namespace esphome::sendspin_ {
|
||||
|
||||
static const char *const TAG = "sendspin.hub";
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
static constexpr uint32_t MDNS_ENABLE_RETRY_MS = 1000;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
// Indexed by the library enums, which start at zero and are contiguous.
|
||||
static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"};
|
||||
@@ -66,26 +62,24 @@ void SendspinHub::setup() {
|
||||
this->client_->add_player(this->player_config_).set_listener(this->player_listener_);
|
||||
#endif
|
||||
|
||||
if (!this->client_->start()) {
|
||||
ESP_LOGE(TAG, "Failed to start Sendspin client");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
#ifndef USE_SENDSPIN_SWITCH
|
||||
this->enabled_ = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void SendspinHub::loop() {
|
||||
if (this->enabled_.has_value() && this->enabled_.value() != this->client_->is_started() &&
|
||||
!this->status_has_error()) {
|
||||
if (!this->enabled_.value()) {
|
||||
this->client_->stop();
|
||||
} else if (!this->client_->start()) {
|
||||
this->status_set_error(LOG_STR("Failed to start Sendspin client"));
|
||||
}
|
||||
}
|
||||
this->client_->loop();
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
// mdns sets up after this hub, so the service is enabled here once mdns is ready. A failed enable retries,
|
||||
// rate limited so a persistent failure does not flood the log or block on the mdns task every loop pass.
|
||||
if (!this->mdns_advertised_ && this->mdns_->is_ready()) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->mdns_enable_attempt_ms_ == 0 || now - this->mdns_enable_attempt_ms_ >= MDNS_ENABLE_RETRY_MS) {
|
||||
this->mdns_enable_attempt_ms_ = now;
|
||||
this->mdns_advertised_ = this->mdns_->set_service_enabled("_sendspin", "_tcp", true);
|
||||
}
|
||||
}
|
||||
this->update_mdns_service_();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -114,25 +108,54 @@ void SendspinHub::dump_config() {
|
||||
#endif
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
void SendspinHub::set_enabled(bool enabled) {
|
||||
if (this->status_has_error()) {
|
||||
ESP_LOGE(TAG, "Cannot %s: Sendspin failed to start, reboot to retry",
|
||||
enabled ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"));
|
||||
return;
|
||||
}
|
||||
this->enabled_ = enabled;
|
||||
}
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinHub::update_mdns_service_() {
|
||||
// Synced from loop() because mdns sets up after this hub and only builds its service list then.
|
||||
if (!this->mdns_->is_ready()) {
|
||||
return;
|
||||
}
|
||||
bool advertise = this->client_->is_started();
|
||||
if (advertise == this->mdns_advertised_) {
|
||||
return;
|
||||
}
|
||||
// One attempt per change
|
||||
this->mdns_advertised_ = advertise;
|
||||
if (!this->mdns_->set_service_enabled("_sendspin", "_tcp", advertise)) {
|
||||
ESP_LOGE(TAG, "Failed to %s mDNS service", advertise ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Delegating methods ---
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
void SendspinHub::connect_to_server(const std::string &url) {
|
||||
if (this->is_ready()) {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->connect_to(url);
|
||||
}
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
void SendspinHub::disconnect_from_server(sendspin::SendspinGoodbyeReason reason) {
|
||||
if (this->is_ready()) {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->disconnect(reason);
|
||||
}
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
|
||||
void SendspinHub::update_state(sendspin::SendspinClientState state) {
|
||||
if (this->is_ready()) {
|
||||
if (this->is_client_running()) {
|
||||
this->client_->update_state(state);
|
||||
}
|
||||
}
|
||||
@@ -251,7 +274,7 @@ void SendspinHub::artwork_frame_done(uint8_t slot) {
|
||||
// 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()) {
|
||||
if (this->is_client_running()) {
|
||||
sendspin::ClientCommandControllerObject obj = {
|
||||
.command = command,
|
||||
.volume = volume,
|
||||
|
||||
@@ -97,7 +97,7 @@ class SendspinHub final : public Component,
|
||||
|
||||
/// @brief Connects the underlying client to the given Sendspin server.
|
||||
///
|
||||
/// No-op if the hub's client is not ready (e.g. setup() has not completed).
|
||||
/// No-op if the hub's client is not running (see is_client_running()).
|
||||
/// Must be called from the main loop thread.
|
||||
/// @param url WebSocket URL of the Sendspin server, starting with `ws://` (e.g. `ws://host:port/path`).
|
||||
void connect_to_server(const std::string &url);
|
||||
@@ -105,7 +105,7 @@ class SendspinHub final : public Component,
|
||||
/// @brief Disconnects the underlying client from the current server.
|
||||
///
|
||||
/// Sends a `client/goodbye` message with the given reason before closing the connection.
|
||||
/// No-op if the hub's client is not ready. Must be called from the main loop thread.
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
/// @param reason Reason reported to the server:
|
||||
/// - `ANOTHER_SERVER`: client is switching to another server.
|
||||
/// - `SHUTDOWN`: client is shutting down.
|
||||
@@ -115,7 +115,7 @@ class SendspinHub final : public Component,
|
||||
|
||||
/// @brief Updates the client's reported playback state on the server.
|
||||
///
|
||||
/// No-op if the hub's client is not ready. Must be called from the main loop thread.
|
||||
/// No-op if the hub's client is not running. Must be called from the main loop thread.
|
||||
/// @param state New client state:
|
||||
/// - `SYNCHRONIZED`: client is synchronized and playing from the server.
|
||||
/// - `ERROR`: client encountered a playback error.
|
||||
@@ -130,6 +130,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; }
|
||||
|
||||
/// @brief Requests the Sendspin client, including the server, the roles and the mDNS advertisement, to start or
|
||||
/// stop.
|
||||
///
|
||||
/// Applied from the hub's loop(). Stopping blocks until the client is fully stopped; the roles' clear callbacks
|
||||
/// fire from inside that call. With a sendspin switch configured the client stays stopped until the switch has
|
||||
/// called this once. Must be called from the main loop thread.
|
||||
void set_enabled(bool enabled);
|
||||
|
||||
/// @brief Returns whether the Sendspin client is running.
|
||||
bool is_client_running() const { return this->client_ != nullptr && this->client_->is_started(); }
|
||||
|
||||
/// @brief Sets the device information reported to the server in the `client/hello` message.
|
||||
///
|
||||
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
|
||||
@@ -212,6 +223,11 @@ class SendspinHub final : public Component,
|
||||
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
|
||||
static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
/// @brief Keeps the `_sendspin` mDNS service advertised while the client is running.
|
||||
void update_mdns_service_();
|
||||
#endif
|
||||
|
||||
// --- SendspinClientListener overrides ---
|
||||
void on_group_update(const sendspin::GroupUpdateObject &group) override;
|
||||
|
||||
@@ -290,6 +306,9 @@ class SendspinHub final : public Component,
|
||||
|
||||
bool task_stack_in_psram_{false};
|
||||
|
||||
// Requested client state, applied from loop(). Empty until the switch restores its state.
|
||||
std::optional<bool> enabled_;
|
||||
|
||||
// Device information sent in the `client/hello` message. Defaults apply when neither the
|
||||
// sendspin configuration nor the project information supplies a value.
|
||||
const char *manufacturer_{"ESPHome"};
|
||||
@@ -298,8 +317,7 @@ class SendspinHub final : public Component,
|
||||
|
||||
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
|
||||
mdns::MDNSComponent *mdns_{nullptr};
|
||||
uint32_t mdns_enable_attempt_ms_{0};
|
||||
bool mdns_advertised_{false};
|
||||
bool mdns_advertised_{false}; // Last state requested from mdns
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_SENDSPIN_ID, SendspinHub, sendspin_ns
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["sendspin"]
|
||||
|
||||
SendspinSwitch = sendspin_ns.class_("SendspinSwitch", switch.Switch, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
switch.switch_schema(
|
||||
SendspinSwitch,
|
||||
block_inverted=True,
|
||||
default_restore_mode="RESTORE_DEFAULT_ON",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
.extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on_esp32,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_SENDSPIN_ID])
|
||||
cg.add_define("USE_SENDSPIN_SWITCH", True)
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "sendspin_switch.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
static const char *const TAG = "sendspin.switch";
|
||||
|
||||
void SendspinSwitch::setup() {
|
||||
// The hub waits for this request, so a restore mode without a state still has to answer.
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(true));
|
||||
}
|
||||
|
||||
void SendspinSwitch::dump_config() { LOG_SWITCH("", "Sendspin Switch", this); }
|
||||
|
||||
// THREAD CONTEXT: Main loop
|
||||
void SendspinSwitch::write_state(bool state) {
|
||||
this->parent_->set_enabled(state);
|
||||
this->publish_state(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/sendspin/sendspin_hub.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
/// @brief Switch that starts and stops the Sendspin client through the hub (see SendspinHub::set_enabled()).
|
||||
class SendspinSwitch final : public switch_::Switch, public SendspinChild {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -400,6 +400,7 @@
|
||||
#define USE_SENDSPIN_CONTROLLER
|
||||
#define USE_SENDSPIN_METADATA
|
||||
#define USE_SENDSPIN_PLAYER
|
||||
#define USE_SENDSPIN_SWITCH
|
||||
#define USE_SENDSPIN_VISUALIZER
|
||||
#define USE_SENDSPIN_PORT 8928 // NOLINT
|
||||
#define USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
packages:
|
||||
sendspin: !include common.yaml
|
||||
|
||||
switch:
|
||||
- platform: sendspin
|
||||
name: "Sendspin Enabled"
|
||||
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
sendspin: !include common-switch.yaml
|
||||
Reference in New Issue
Block a user