diff --git a/CODEOWNERS b/CODEOWNERS index 822b0e973c1..f4b288b23d6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -443,6 +443,7 @@ esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct esphome/components/sendspin/* @kahrendt esphome/components/sendspin/media_player/* @kahrendt esphome/components/sendspin/media_source/* @kahrendt +esphome/components/sendspin/text_sensor/* @kahrendt esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/serial_proxy/* @kbx81 diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 25e541a4938..da298feb86d 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -39,6 +39,10 @@ void SendspinHub::setup() { this->controller_role_->set_listener(this); #endif +#ifdef USE_SENDSPIN_METADATA + this->client_->add_metadata().set_listener(this); +#endif + #ifdef USE_SENDSPIN_PLAYER this->client_->add_player(this->player_config_).set_listener(this->player_listener_); #endif @@ -167,6 +171,13 @@ void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObjec } #endif +#ifdef USE_SENDSPIN_METADATA +// THREAD CONTEXT: Main loop (MetadataRoleListener override, fired from client_->loop()) +void SendspinHub::on_metadata(const sendspin::ServerMetadataStateObject &metadata) { + this->metadata_update_callbacks_.call(metadata); +} +#endif + #ifdef USE_SENDSPIN_PLAYER // THREAD CONTEXT: Main loop, called from child component setup() after player role is created and configured sendspin::PlayerRole *SendspinHub::get_player_role() { diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index c9266bd4d1e..8d9c58a3abb 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -16,6 +16,9 @@ #ifdef USE_SENDSPIN_CONTROLLER #include #endif +#ifdef USE_SENDSPIN_METADATA +#include +#endif #ifdef USE_SENDSPIN_PLAYER #include #endif @@ -66,6 +69,9 @@ struct StaticDelayPref { class SendspinHub final : public Component, #ifdef USE_SENDSPIN_CONTROLLER public sendspin::ControllerRoleListener, +#endif +#ifdef USE_SENDSPIN_METADATA + public sendspin::MetadataRoleListener, #endif public sendspin::SendspinClientListener, public sendspin::SendspinNetworkProvider, @@ -122,6 +128,12 @@ class SendspinHub final : public Component, } #endif +#ifdef USE_SENDSPIN_METADATA + template void add_metadata_update_callback(F &&callback) { + this->metadata_update_callbacks_.add(std::forward(callback)); + } +#endif + #ifdef USE_SENDSPIN_PLAYER void set_listener(sendspin::PlayerRoleListener *listener) { this->player_listener_ = listener; } void set_player_config(const sendspin::PlayerRoleConfig &config) { this->player_config_ = config; } @@ -159,6 +171,13 @@ class SendspinHub final : public Component, CallbackManager controller_state_callbacks_{}; #endif +#ifdef USE_SENDSPIN_METADATA + void on_metadata(const sendspin::ServerMetadataStateObject &metadata) override; + + // Callback fan-out to child components; they filter as needed + CallbackManager metadata_update_callbacks_{}; +#endif + #ifdef USE_SENDSPIN_PLAYER sendspin::PlayerRoleListener *player_listener_{nullptr}; sendspin::PlayerRoleConfig player_config_{}; diff --git a/esphome/components/sendspin/text_sensor/__init__.py b/esphome/components/sendspin/text_sensor/__init__.py new file mode 100644 index 00000000000..b7f216ca0ce --- /dev/null +++ b/esphome/components/sendspin/text_sensor/__init__.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TYPE +from esphome.types import ConfigType + +from .. import CONF_SENDSPIN_ID, SendspinHub, request_metadata_support, sendspin_ns + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["sendspin"] + +SendspinTextSensor = sendspin_ns.class_( + "SendspinTextSensor", + text_sensor.TextSensor, + cg.Component, +) + +SendspinTextMetadataTypes = sendspin_ns.enum("SendspinTextMetadataTypes", is_class=True) +SENDSPIN_TEXT_METADATA_TYPES = { + "title": SendspinTextMetadataTypes.TITLE, + "artist": SendspinTextMetadataTypes.ARTIST, + "album": SendspinTextMetadataTypes.ALBUM, + "album_artist": SendspinTextMetadataTypes.ALBUM_ARTIST, + "year": SendspinTextMetadataTypes.YEAR, + "track": SendspinTextMetadataTypes.TRACK, +} + + +def _request_roles(config: ConfigType) -> ConfigType: + """Request the necessary Sendspin roles for the text sensor.""" + request_metadata_support() + + return config + + +CONFIG_SCHEMA = cv.All( + text_sensor.text_sensor_schema().extend( + { + cv.GenerateID(): cv.declare_id(SendspinTextSensor), + cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub), + cv.Required(CONF_TYPE): cv.enum(SENDSPIN_TEXT_METADATA_TYPES), + } + ), + cv.only_on_esp32, + _request_roles, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_SENDSPIN_ID]) + await text_sensor.register_text_sensor(var, config) + + cg.add(var.set_metadata_type(config[CONF_TYPE])) diff --git a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp new file mode 100644 index 00000000000..d16d51f63c6 --- /dev/null +++ b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.cpp @@ -0,0 +1,85 @@ +#include "sendspin_text_sensor.h" + +#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_TEXT_SENSOR) + +#include "esphome/core/helpers.h" + +#include + +#include + +namespace esphome::sendspin_ { + +static const char *const TAG = "sendspin.text_sensor"; + +void SendspinTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sendspin", this); } + +// THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop +// (SendspinHub dispatches metadata from client_->loop()). +void SendspinTextSensor::setup() { + switch (this->metadata_type_) { + case SendspinTextMetadataTypes::TITLE: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.title.has_value()) { + this->publish_if_changed_(metadata.title.value().c_str()); + } + }); + break; + } + case SendspinTextMetadataTypes::ARTIST: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.artist.has_value()) { + this->publish_if_changed_(metadata.artist.value().c_str()); + } + }); + break; + } + case SendspinTextMetadataTypes::ALBUM: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.album.has_value()) { + this->publish_if_changed_(metadata.album.value().c_str()); + } + }); + break; + } + case SendspinTextMetadataTypes::ALBUM_ARTIST: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.album_artist.has_value()) { + this->publish_if_changed_(metadata.album_artist.value().c_str()); + } + }); + break; + } + case SendspinTextMetadataTypes::YEAR: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.year.has_value() && metadata.year.value() <= 9999) { + char buf[UINT32_MAX_STR_SIZE]; + uint32_to_str(buf, metadata.year.value()); + this->publish_if_changed_(buf); + } + }); + break; + } + case SendspinTextMetadataTypes::TRACK: { + this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) { + if (metadata.track.has_value() && metadata.track.value() <= 9999) { + char buf[UINT32_MAX_STR_SIZE]; + uint32_to_str(buf, metadata.track.value()); + this->publish_if_changed_(buf); + } + }); + break; + } + } +} + +// Dedup to avoid frontend churn; TextSensor::publish_state already dedups the string assign but still notifies. +void SendspinTextSensor::publish_if_changed_(const char *value) { + if (this->get_raw_state() != value) { + this->publish_state(value); + } +} + +} // namespace esphome::sendspin_ + +#endif diff --git a/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h new file mode 100644 index 00000000000..d9ef49c938c --- /dev/null +++ b/esphome/components/sendspin/text_sensor/sendspin_text_sensor.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/defines.h" + +#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_TEXT_SENSOR) + +#include "esphome/components/sendspin/sendspin_hub.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome::sendspin_ { + +enum class SendspinTextMetadataTypes { + TITLE, + ARTIST, + ALBUM, + ALBUM_ARTIST, + YEAR, + TRACK, +}; + +class SendspinTextSensor : public SendspinChild, public text_sensor::TextSensor { + public: + void dump_config() override; + void setup() override; + + void set_metadata_type(SendspinTextMetadataTypes metadata_type) { this->metadata_type_ = metadata_type; } + + protected: + void publish_if_changed_(const char *value); + + SendspinTextMetadataTypes metadata_type_; +}; + +} // namespace esphome::sendspin_ +#endif diff --git a/tests/components/sendspin/common-text_sensor.yaml b/tests/components/sendspin/common-text_sensor.yaml new file mode 100644 index 00000000000..0bfbf457574 --- /dev/null +++ b/tests/components/sendspin/common-text_sensor.yaml @@ -0,0 +1,21 @@ +<<: !include common.yaml + +text_sensor: + - platform: sendspin + name: "Title" + type: title + - platform: sendspin + name: "Artist" + type: artist + - platform: sendspin + name: "Album" + type: album + - platform: sendspin + name: "Album Artist" + type: album_artist + - platform: sendspin + name: "Year" + type: year + - platform: sendspin + name: "Track Number" + type: track diff --git a/tests/components/sendspin/test-text_sensor.esp32-idf.yaml b/tests/components/sendspin/test-text_sensor.esp32-idf.yaml new file mode 100644 index 00000000000..8998b8896ef --- /dev/null +++ b/tests/components/sendspin/test-text_sensor.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common-text_sensor.yaml