ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
sendspin_sensor.cpp
Go to the documentation of this file.
1#include "sendspin_sensor.h"
2
3#if defined(USE_ESP32) && defined(USE_SENDSPIN_METADATA) && defined(USE_SENSOR)
4
5#include <sendspin/metadata_role.h>
6
7#include <cmath>
8
9namespace esphome::sendspin_ {
10
11static const char *const TAG = "sendspin.sensor";
12
13// --- SendspinTrackProgressSensor ---
14
16 LOG_SENSOR("", "Track Progress", this);
17 LOG_UPDATE_INTERVAL(this);
18}
19
20// THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop
21// (SendspinHub dispatches metadata from client_->loop()).
23 this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) {
24 if (!metadata.progress.has_value()) {
25 // Progress is unknown: the server has not reported it, or it was cleared (e.g. on disconnect). Stop polling and
26 // report unknown rather than leaving the last position frozen on the frontend. Only the transition is published;
27 // NAN never compares equal to itself, so an unguarded publish would repeat on every metadata update.
28 this->stop_poller();
29 if (!std::isnan(this->get_raw_state())) {
30 this->publish_state(NAN);
31 }
32 return;
33 }
34 const auto &progress = metadata.progress.value();
35 if (progress.playback_speed == 0) {
36 // Paused: freeze progress at the reported position and stop polling to save cycles.
37 this->stop_poller();
38 this->publish_state(progress.track_progress);
39 } else {
40 // Resumed: publish the fresh interpolated position immediately so the frontend doesn't show a stale
41 // paused value until the next poll tick.
42 this->publish_state(this->parent_->get_track_progress_ms());
43 this->start_poller();
44 }
45 });
46
47 // PollingComponent starts the poller before setup(), but there is nothing to interpolate yet:
48 // get_track_progress_ms() returns 0 until the server reports a position, so polling now would publish 0 every tick
49 // from boot until the first metadata arrives. The callback above starts it once playback is running.
50 this->stop_poller();
51}
52
53// THREAD CONTEXT: Main loop.
54// Sendspin only pushes progress on state changes (play/pause/seek/speed change), not continuously during
55// playback. The hub helper interpolates the current position from the last server update and the playback
56// speed, giving us a fresh value on every poll.
57void SendspinTrackProgressSensor::update() { this->publish_state(this->parent_->get_track_progress_ms()); }
58
59// --- SendspinMetadataSensor ---
60
62 switch (this->metadata_type_) {
64 LOG_SENSOR("", "Track Duration", this);
65 break;
67 LOG_SENSOR("", "Year", this);
68 break;
70 LOG_SENSOR("", "Track", this);
71 break;
72 }
73}
74
75std::optional<float> SendspinMetadataSensor::extract_value_(const sendspin::ServerMetadataStateObject &metadata) const {
76 switch (this->metadata_type_) {
78 if (metadata.progress.has_value())
79 return metadata.progress.value().track_duration;
80 return std::nullopt;
82 if (metadata.year.has_value())
83 return metadata.year.value();
84 return std::nullopt;
86 if (metadata.track.has_value())
87 return metadata.track.value();
88 return std::nullopt;
89 }
90 return std::nullopt;
91}
92
93// THREAD CONTEXT: Main loop. The registered metadata callback also fires on the main loop
94// (SendspinHub dispatches metadata from client_->loop()).
96 this->parent_->add_metadata_update_callback([this](const sendspin::ServerMetadataStateObject &metadata) {
97 // A field the server has not provided, or has explicitly cleared, is published as NAN (the sensor convention for
98 // unknown) rather than skipped, so a value that goes away does not linger from the previous track.
99 this->publish_if_changed_(this->extract_value_(metadata).value_or(NAN));
100 });
101}
102
103// Dedup to avoid frontend churn; Sensor::publish_state always notifies without checking for changes.
105 const float current = this->get_raw_state();
106 // The raw state starts as NAN, so a field that is already cleared when the first update arrives is suppressed here
107 // as well: the frontend still shows the sensor as unknown, which is what a clear means. NAN never compares equal to
108 // itself, so a field that stays cleared would republish on every metadata update without the second check.
109 if (current != value && !(std::isnan(current) && std::isnan(value))) {
110 this->publish_state(value);
111 }
112}
113
114} // namespace esphome::sendspin_
115
116#endif
SendspinNumericMetadataTypes metadata_type_
std::optional< float > extract_value_(const sendspin::ServerMetadataStateObject &metadata) const
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:68
float get_raw_state() const
Getter-syntax for .raw_state.
Definition sensor.h:100