add Sendspin sensor component

This commit is contained in:
Kevin Ahrendt
2026-04-24 08:21:29 -04:00
parent ac7f0f0b74
commit 42f0b1a7e2
8 changed files with 181 additions and 1 deletions
+1
View File
@@ -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/sensor/* @kahrendt
esphome/components/sendspin/text_sensor/* @kahrendt
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
+10 -1
View File
@@ -40,7 +40,8 @@ void SendspinHub::setup() {
#endif
#ifdef USE_SENDSPIN_METADATA
this->client_->add_metadata().set_listener(this);
this->metadata_role_ = &this->client_->add_metadata();
this->metadata_role_->set_listener(this);
#endif
#ifdef USE_SENDSPIN_PLAYER
@@ -176,6 +177,14 @@ void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObjec
void SendspinHub::on_metadata(const sendspin::ServerMetadataStateObject &metadata) {
this->metadata_update_callbacks_.call(metadata);
}
// THREAD CONTEXT: Main loop (invoked from Sendspin components)
uint32_t SendspinHub::get_track_progress_ms() {
if (this->is_ready()) {
return this->metadata_role_->get_track_progress_ms();
}
return 0;
}
#endif
#ifdef USE_SENDSPIN_PLAYER
@@ -132,6 +132,9 @@ class SendspinHub final : public Component,
template<typename F> void add_metadata_update_callback(F &&callback) {
this->metadata_update_callbacks_.add(std::forward<F>(callback));
}
/// @brief Returns the interpolated track progress in milliseconds, or 0 if the hub is not yet ready.
uint32_t get_track_progress_ms();
#endif
#ifdef USE_SENDSPIN_PLAYER
@@ -172,6 +175,8 @@ class SendspinHub final : public Component,
#endif
#ifdef USE_SENDSPIN_METADATA
sendspin::MetadataRole *metadata_role_{nullptr};
void on_metadata(const sendspin::ServerMetadataStateObject &metadata) override;
// Callback fan-out to child components; they filter as needed
@@ -0,0 +1,62 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TYPE, STATE_CLASS_MEASUREMENT, UNIT_MILLISECOND
from esphome.types import ConfigType
from .. import CONF_SENDSPIN_ID, SendspinHub, request_metadata_support, sendspin_ns
CODEOWNERS = ["@kahrendt"]
DEPENDENCIES = ["sendspin"]
SendspinTrackProgressSensor = sendspin_ns.class_(
"SendspinTrackProgressSensor",
sensor.Sensor,
cg.PollingComponent,
)
SendspinTrackDurationSensor = sendspin_ns.class_(
"SendspinTrackDurationSensor",
sensor.Sensor,
cg.Component,
)
def _request_roles(config: ConfigType) -> ConfigType:
"""Request the necessary Sendspin roles for the sensor."""
request_metadata_support()
return config
_SENSOR_SCHEMA = sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_MILLISECOND,
).extend(
{
cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub),
}
)
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"track_progress": _SENSOR_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(SendspinTrackProgressSensor)}
).extend(cv.polling_component_schema("1s")),
"track_duration": _SENSOR_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(SendspinTrackDurationSensor)}
).extend(cv.COMPONENT_SCHEMA),
},
key=CONF_TYPE,
),
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 sensor.register_sensor(var, config)
@@ -0,0 +1,62 @@
#include "sendspin_sensor.h"
#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_SENSOR)
#include <sendspin/metadata_role.h>
namespace esphome::sendspin_ {
static const char *const TAG = "sendspin.sensor";
// --- SendspinTrackProgressSensor ---
void SendspinTrackProgressSensor::dump_config() { LOG_SENSOR("", "Sendspin Track Progress", this); }
// THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop
// (SendspinHub dispatches metadata from client_->loop()).
void SendspinTrackProgressSensor::setup() {
this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) {
if (!metadata.progress.has_value()) {
return;
}
const auto &progress = metadata.progress.value();
if (progress.playback_speed == 0) {
// Paused: freeze progress at the reported position and stop polling to save cycles.
this->stop_poller();
this->publish_state(progress.track_progress);
} else {
this->start_poller();
}
});
}
// THREAD CONTEXT: Main loop.
// Sendspin only pushes progress on state changes (play/pause/seek/speed change), not continuously during
// playback. The hub helper interpolates the current position from the last server update and the playback
// speed, giving us a fresh value on every poll.
void SendspinTrackProgressSensor::update() { this->publish_state(this->parent_->get_track_progress_ms()); }
// --- SendspinTrackDurationSensor ---
void SendspinTrackDurationSensor::dump_config() { LOG_SENSOR("", "Sendspin Track Duration", this); }
// THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop
// (SendspinHub dispatches metadata from client_->loop()).
void SendspinTrackDurationSensor::setup() {
this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) {
if (metadata.progress.has_value()) {
this->publish_if_changed_(metadata.progress.value().track_duration);
}
});
}
// Dedup to avoid frontend churn; Sensor::publish_state always notifies without checking for changes.
void SendspinTrackDurationSensor::publish_if_changed_(float value) {
if (this->get_raw_state() != value) {
this->publish_state(value);
}
}
} // namespace esphome::sendspin_
#endif
@@ -0,0 +1,31 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_SENSOR)
#include "esphome/components/sendspin/sendspin_hub.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::sendspin_ {
class SendspinTrackProgressSensor : public sensor::Sensor, public PollingComponent, public Parented<SendspinHub> {
public:
float get_setup_priority() const override { return sendspin_priority::CHILD; }
void dump_config() override;
void setup() override;
void update() override;
};
class SendspinTrackDurationSensor : public sensor::Sensor, public Component, public Parented<SendspinHub> {
public:
float get_setup_priority() const override { return sendspin_priority::CHILD; }
void dump_config() override;
void setup() override;
protected:
void publish_if_changed_(float value);
};
} // namespace esphome::sendspin_
#endif
@@ -0,0 +1,9 @@
<<: !include common.yaml
sensor:
- platform: sendspin
name: "Sendspin Track Progress"
type: track_progress
- platform: sendspin
name: "Sendspin Track Duration"
type: track_duration
@@ -0,0 +1 @@
<<: !include common-sensor.yaml