mirror of
https://github.com/esphome/esphome.git
synced 2026-05-24 09:56:46 +08:00
[sendspin] Add metadata sensor component (#15971)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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() const {
|
||||
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() const;
|
||||
#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
|
||||
@@ -211,6 +216,16 @@ class SendspinChild : public Component, public Parented<SendspinHub> {
|
||||
float get_setup_priority() const override { return sendspin_priority::CHILD; }
|
||||
};
|
||||
|
||||
/// @brief Base class for sendspin subcomponents that need polling behavior.
|
||||
///
|
||||
/// Same purpose as SendspinChild but inherits from PollingComponent for subcomponents
|
||||
/// that poll on a fixed interval. Subcomponents should inherit from this instead of
|
||||
/// listing PollingComponent/Parented individually and must not override get_setup_priority().
|
||||
class SendspinPollingChild : public PollingComponent, public Parented<SendspinHub> {
|
||||
public:
|
||||
float get_setup_priority() const override { return sendspin_priority::CHILD; }
|
||||
};
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_TYPE,
|
||||
CONF_YEAR,
|
||||
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"]
|
||||
|
||||
CONF_TRACK = "track"
|
||||
CONF_TRACK_PROGRESS = "track_progress"
|
||||
CONF_TRACK_DURATION = "track_duration"
|
||||
|
||||
SendspinTrackProgressSensor = sendspin_ns.class_(
|
||||
"SendspinTrackProgressSensor",
|
||||
sensor.Sensor,
|
||||
cg.PollingComponent,
|
||||
)
|
||||
SendspinMetadataSensor = sendspin_ns.class_(
|
||||
"SendspinMetadataSensor",
|
||||
sensor.Sensor,
|
||||
cg.Component,
|
||||
)
|
||||
|
||||
SendspinNumericMetadataTypes = sendspin_ns.enum(
|
||||
"SendspinNumericMetadataTypes", is_class=True
|
||||
)
|
||||
_METADATA_TYPE_ENUM = {
|
||||
CONF_TRACK_DURATION: SendspinNumericMetadataTypes.TRACK_DURATION,
|
||||
CONF_YEAR: SendspinNumericMetadataTypes.YEAR,
|
||||
CONF_TRACK: SendspinNumericMetadataTypes.TRACK,
|
||||
}
|
||||
|
||||
|
||||
def _request_roles(config: ConfigType) -> ConfigType:
|
||||
"""Request the necessary Sendspin roles for the sensor."""
|
||||
request_metadata_support()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
_HUB_ID_SCHEMA = cv.Schema({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)})
|
||||
|
||||
|
||||
def _metadata_schema(**sensor_kwargs):
|
||||
"""Schema for event-driven numeric metadata sensors (duration/year/track)."""
|
||||
return (
|
||||
sensor.sensor_schema(
|
||||
SendspinMetadataSensor,
|
||||
accuracy_decimals=0,
|
||||
**sensor_kwargs,
|
||||
)
|
||||
.extend(_HUB_ID_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.typed_schema(
|
||||
{
|
||||
CONF_TRACK_PROGRESS: sensor.sensor_schema(
|
||||
SendspinTrackProgressSensor,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_MILLISECOND,
|
||||
)
|
||||
.extend(_HUB_ID_SCHEMA)
|
||||
.extend(cv.polling_component_schema("1s")),
|
||||
CONF_TRACK_DURATION: _metadata_schema(
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
unit_of_measurement=UNIT_MILLISECOND,
|
||||
),
|
||||
CONF_YEAR: _metadata_schema(),
|
||||
CONF_TRACK: _metadata_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)
|
||||
|
||||
if (metadata_type := _METADATA_TYPE_ENUM.get(config[CONF_TYPE])) is not None:
|
||||
cg.add(var.set_metadata_type(metadata_type))
|
||||
@@ -0,0 +1,98 @@
|
||||
#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("", "Track Progress", this);
|
||||
LOG_UPDATE_INTERVAL(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 {
|
||||
// Resumed: publish the fresh interpolated position immediately so the frontend doesn't show a stale
|
||||
// paused value until the next poll tick.
|
||||
this->publish_state(this->parent_->get_track_progress_ms());
|
||||
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()); }
|
||||
|
||||
// --- SendspinMetadataSensor ---
|
||||
|
||||
void SendspinMetadataSensor::dump_config() {
|
||||
switch (this->metadata_type_) {
|
||||
case SendspinNumericMetadataTypes::TRACK_DURATION:
|
||||
LOG_SENSOR("", "Track Duration", this);
|
||||
break;
|
||||
case SendspinNumericMetadataTypes::YEAR:
|
||||
LOG_SENSOR("", "Year", this);
|
||||
break;
|
||||
case SendspinNumericMetadataTypes::TRACK:
|
||||
LOG_SENSOR("", "Track", this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<float> SendspinMetadataSensor::extract_value_(const sendspin::ServerMetadataStateObject &metadata) const {
|
||||
switch (this->metadata_type_) {
|
||||
case SendspinNumericMetadataTypes::TRACK_DURATION:
|
||||
if (metadata.progress.has_value())
|
||||
return metadata.progress.value().track_duration;
|
||||
return std::nullopt;
|
||||
case SendspinNumericMetadataTypes::YEAR:
|
||||
if (metadata.year.has_value())
|
||||
return metadata.year.value();
|
||||
return std::nullopt;
|
||||
case SendspinNumericMetadataTypes::TRACK:
|
||||
if (metadata.track.has_value())
|
||||
return metadata.track.value();
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop
|
||||
// (SendspinHub dispatches metadata from client_->loop()).
|
||||
void SendspinMetadataSensor::setup() {
|
||||
this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) {
|
||||
if (auto value = this->extract_value_(metadata)) {
|
||||
this->publish_if_changed_(*value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Dedup to avoid frontend churn; Sensor::publish_state always notifies without checking for changes.
|
||||
void SendspinMetadataSensor::publish_if_changed_(float value) {
|
||||
if (this->get_raw_state() != value) {
|
||||
this->publish_state(value);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
#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"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace esphome::sendspin_ {
|
||||
|
||||
class SendspinTrackProgressSensor : public sensor::Sensor, public SendspinPollingChild {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
void update() override;
|
||||
};
|
||||
|
||||
enum class SendspinNumericMetadataTypes {
|
||||
TRACK_DURATION,
|
||||
YEAR,
|
||||
TRACK,
|
||||
};
|
||||
|
||||
class SendspinMetadataSensor : public sensor::Sensor, public SendspinChild {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
|
||||
void set_metadata_type(SendspinNumericMetadataTypes metadata_type) { this->metadata_type_ = metadata_type; }
|
||||
|
||||
protected:
|
||||
std::optional<float> extract_value_(const sendspin::ServerMetadataStateObject &metadata) const;
|
||||
void publish_if_changed_(float value);
|
||||
|
||||
SendspinNumericMetadataTypes metadata_type_;
|
||||
};
|
||||
|
||||
} // namespace esphome::sendspin_
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
sensor:
|
||||
- platform: sendspin
|
||||
name: "Sendspin Track Progress"
|
||||
type: track_progress
|
||||
- platform: sendspin
|
||||
name: "Sendspin Track Duration"
|
||||
type: track_duration
|
||||
- platform: sendspin
|
||||
name: "Sendspin Year"
|
||||
type: year
|
||||
- platform: sendspin
|
||||
name: "Sendspin Track"
|
||||
type: track
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common-sensor.yaml
|
||||
Reference in New Issue
Block a user