ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
sendspin_hub.cpp
Go to the documentation of this file.
1#include "sendspin_hub.h"
2
3#ifdef USE_ESP_IDF
4
6#ifdef USE_ETHERNET
8#endif
9#ifdef USE_WIFI
11#endif
12
15#include "esphome/core/log.h"
17
18#include <esp_log.h>
19
20#include <cstring>
21#include <memory>
22
23namespace esphome::sendspin_ {
24
25ESPHOME_LOG_TAG(TAG, "sendspin.hub");
26
27namespace {
28
29// The reason passed to on_pairing_failed automations, in the protocol's own spelling.
30StringRef pair_abort_reason_to_string(sendspin::SendspinPairAbortReason reason) {
31 using sendspin::SendspinPairAbortReason;
32 switch (reason) {
33 case SendspinPairAbortReason::ATTEMPT_TIMEOUT:
34 return StringRef::from_lit("attempt_timeout");
35 case SendspinPairAbortReason::CONCURRENT_ATTEMPT:
36 return StringRef::from_lit("concurrent_attempt");
37 case SendspinPairAbortReason::METHOD_NOT_SUPPORTED:
38 return StringRef::from_lit("method_not_supported");
39 case SendspinPairAbortReason::PAIRING_CODE_MISMATCH:
40 return StringRef::from_lit("pairing_code_mismatch");
41 case SendspinPairAbortReason::USER_CANCELLED:
42 return StringRef::from_lit("user_cancelled");
43 case SendspinPairAbortReason::UNKNOWN:
44 break;
45 }
46 return StringRef::from_lit("unknown");
47}
48
49// Zeroes through a volatile pointer so dead-store elimination cannot drop it.
50void secure_wipe(void *data, size_t len) {
51 volatile auto *p = static_cast<volatile uint8_t *>(data);
52 for (size_t i = 0; i < len; ++i) {
53 p[i] = 0;
54 }
55}
56
57// Call once per key, from setup().
58ESPPreferenceObject make_blob_pref(const std::string &name, size_t size) {
59 return global_preferences->make_preference(size, fnv1a_hash("sendspin_" + name));
60}
61
62// Loads straight into the returned buffer, which the library wipes after use. A failed load can
63// leave part of a stored private key or PSK behind, so that path wipes it here. This only covers
64// this buffer: the preference backend keeps its own copy of a pending write until it syncs.
65std::optional<std::vector<uint8_t>> read_blob(ESPPreferenceObject &pref, size_t size) {
66 std::vector<uint8_t> out(size);
67 if (!pref.load(out.data(), out.size())) {
68 secure_wipe(out.data(), out.size());
69 return std::nullopt;
70 }
71 return out;
72}
73
74// The library always writes a key's fixed size; any other length is misuse and is not stored.
75bool write_blob(ESPPreferenceObject &pref, const char *key, size_t size, const uint8_t *data, size_t len) {
76 if (len != size) {
77 ESP_LOGW(TAG, "\"%s\" blob of %zu bytes does not match its size of %zu; rejecting write", key, len, size);
78 return false;
79 }
80 if (!pref.save(data, len)) {
81 ESP_LOGW(TAG, "Failed to persist \"%s\" blob (%zu bytes)", key, len);
82 return false;
83 }
84 return true;
85}
86
87// "rec_<n>" to n; nullopt for any other key or a slot with no storage here.
88std::optional<size_t> parse_record_slot(const std::string &key) {
89 const char *const prefix = sendspin::persistence_keys::RECORD_SLOT_PREFIX;
90 const size_t prefix_len = std::strlen(prefix);
91 if (key.size() <= prefix_len || key.compare(0, prefix_len, prefix) != 0) {
92 return std::nullopt;
93 }
94 size_t slot = 0;
95 for (size_t i = prefix_len; i < key.size(); i++) {
96 const char c = key[i];
97 if (c < '0' || c > '9') {
98 return std::nullopt;
99 }
100 slot = slot * 10 + static_cast<size_t>(c - '0');
101 if (slot >= SENDSPIN_RECORD_SLOTS) {
102 return std::nullopt;
103 }
104 }
105 return slot;
106}
107
108} // namespace
109
110#ifdef USE_SENDSPIN_ARTWORK
111// Indexed by the library enums, which start at zero and are contiguous.
112static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"};
113static const char *const IMAGE_FORMAT_NAMES[] = {"JPEG", "PNG"};
114#endif
115
116SendspinHub *global_sendspin_hub = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
117
119
121 auto config = this->build_client_config_();
122 this->client_ = std::make_unique<sendspin::SendspinClient>(std::move(config));
123
124 // Set up persistence (preferences must be initialized before providers are added to the client). These key names
125 // are frozen; see PREFERENCE KEYS in sendspin_hub.h.
126 namespace keys = sendspin::persistence_keys;
127 this->keypair_pref_ = make_blob_pref("keypair", keys::KEYPAIR_SIZE);
128 this->pairing_psk_pref_ = make_blob_pref("pair_psk", keys::PAIRING_PSK_SIZE);
129 this->last_played_pref_ = make_blob_pref("last_played", keys::LAST_PLAYED_SIZE);
130 // Released firmware's name, so a calibrated delay survives.
131 this->output_delay_pref_ = make_blob_pref("static_delay", keys::OUTPUT_DELAY_SIZE);
132 for (size_t slot = 0; slot < SENDSPIN_RECORD_SLOTS; slot++) {
133 this->record_slot_prefs_[slot] = make_blob_pref(keys::record_slot_key(slot), keys::RECORD_SLOT_SIZE);
134 }
135 this->record_order_pref_ = make_blob_pref(keys::RECORD_ORDER, SENDSPIN_RECORD_SLOTS);
136
137 // Wire providers and client listener
138 this->client_->set_listener(this);
139 this->client_->set_network_provider(this);
140 this->client_->set_persistence_provider(this);
141
142#ifdef USE_SENDSPIN_ARTWORK
143 this->artwork_role_ = &this->client_->add_artwork(this->artwork_config_);
144 this->artwork_role_->set_listener(this);
145#endif
146
147#ifdef USE_SENDSPIN_CONTROLLER
148 this->controller_role_ = &this->client_->add_controller();
149 this->controller_role_->set_listener(this);
150#endif
151
152#ifdef USE_SENDSPIN_METADATA
153 this->metadata_role_ = &this->client_->add_metadata();
154 this->metadata_role_->set_listener(this);
155#endif
156
157#ifdef USE_SENDSPIN_PLAYER
158 this->client_->add_player(this->player_config_).set_listener(this->player_listener_);
159#endif
160
161 // Set before setup() by codegen; an unpaired access switch sets it later, from its own setup().
162 if (this->unpaired_access_.has_value()) {
163 this->client_->set_unpaired_access_enabled(*this->unpaired_access_);
164 }
165}
166
168 if (this->enabled_.has_value() && this->unpaired_access_.has_value() &&
169 this->enabled_.value() != this->client_->is_started() && !this->status_has_error()) {
170 if (!this->enabled_.value()) {
171 this->client_->stop();
172 } else if (!this->client_->start()) {
173 this->status_set_error(LOG_STR("Failed to start Sendspin client"));
174 }
175 }
176 this->client_->loop();
177
178#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
179 this->update_mdns_service_();
180#endif
181}
182
183// Sends each server a goodbye and writes what the client still owed its provider. Wi-Fi sets up
184// before this hub, so it shuts down after it and the goodbyes can still go out.
186 // Keeps loop() from starting the client again.
187 this->enabled_ = false;
188 if (this->is_client_running()) {
189 this->client_->stop();
190 }
191}
192
194 // client_id exists only once start() has run.
195 const char *client_id = "(unavailable)";
196 if (this->client_ != nullptr && !this->client_->client_id().empty()) {
197 client_id = this->client_->client_id().c_str();
198 }
199 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
200 ESP_LOGCONFIG(TAG,
201 "Sendspin Hub:\n"
202 " Client ID: %s\n"
203 " MAC address: %s\n"
204 " Manufacturer: %s\n"
205 " Model: %s\n"
206 " Firmware version: %s\n"
207 " Task stack in PSRAM: %s\n"
208 " Unpaired access: %s\n"
209 " Pairing code method: %s",
210 client_id, get_mac_address_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(),
211 this->firmware_version_, YESNO(this->task_stack_in_psram_),
212 YESNO(this->client_ != nullptr && this->client_->is_unpaired_access_enabled()),
213 this->pairing_code_method_());
214
215#ifdef USE_SENDSPIN_ARTWORK
216 // Slot indices come from the order the image platform entries were declared, so the log is the
217 // only place the mapping from a slot to the artwork it asked for can be read back.
218 uint8_t slot = 0;
219 for (const auto &preference : this->artwork_config_.preferred_formats) {
220 ESP_LOGCONFIG(TAG, " Artwork slot %u: %s as %s, %ux%u, display offset %" PRId32 " ms", slot++,
221 IMAGE_SOURCE_NAMES[static_cast<uint8_t>(preference.source)],
222 IMAGE_FORMAT_NAMES[static_cast<uint8_t>(preference.format)], preference.width, preference.height,
223 preference.display_offset_ms);
224 }
225#endif
226}
227
228// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components)
229void SendspinHub::set_enabled(bool enabled) {
230 if (this->status_has_error()) {
231 ESP_LOGE(TAG, "Cannot %s: Sendspin failed to start, reboot to retry",
232 enabled ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"));
233 return;
234 }
235 this->enabled_ = enabled;
236}
237
238// THREAD CONTEXT: Main loop
239std::optional<std::string> SendspinHub::get_pairing_token() const {
240 if (this->client_ == nullptr) {
241 return std::nullopt;
242 }
243 return this->client_->pairing_token();
244}
245
246// THREAD CONTEXT: Main loop
248 if (this->client_ == nullptr) {
249 return StringRef();
250 }
251 return StringRef(this->client_->client_id());
252}
253
254// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components)
256 this->unpaired_access_ = enabled;
257 // Before setup() there is no client yet; setup() applies the stored value.
258 if (this->client_ != nullptr) {
259 this->client_->set_unpaired_access_enabled(enabled);
260 }
261}
262
263#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
264// THREAD CONTEXT: Main loop
266 // Synced from loop() because mdns sets up after this hub and only builds its service list then.
267 if (!this->mdns_->is_ready()) {
268 return;
269 }
270 bool advertise = this->client_->is_started();
271 if (advertise == this->mdns_advertised_) {
272 return;
273 }
274 // One attempt per change
275 this->mdns_advertised_ = advertise;
276 if (!this->mdns_->set_service_enabled("_sendspin", "_tcp", advertise)) {
277 ESP_LOGE(TAG, "Failed to %s mDNS service", advertise ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"));
278 }
279}
280#endif
281
282// --- Delegating methods ---
283
284// THREAD CONTEXT: Main loop (invoked from Sendspin components)
285void SendspinHub::connect_to_server(const std::string &url) {
286 if (this->is_client_running()) {
287 this->client_->connect_to(url);
288 }
289}
290
291// THREAD CONTEXT: Main loop (invoked from Sendspin components)
292void SendspinHub::disconnect_from_server(sendspin::SendspinGoodbyeReason reason) {
293 if (this->is_client_running()) {
294 this->client_->disconnect(reason);
295 }
296}
297
298// THREAD CONTEXT: Main loop (invoked from Sendspin components)
300 if (this->is_client_running() &&
301 this->client_->get_group_state().playback_state == sendspin::SendspinPlaybackState::PLAYING) {
302 this->client_->leave();
303 }
304}
305
306// THREAD CONTEXT: Main loop (invoked from the sendspin.confirm_pairing_window action)
308 if (this->is_client_running()) {
309 this->client_->confirm_pairing_window();
310 }
311}
312
313// THREAD CONTEXT: Main loop (invoked from the sendspin.cancel_pairing_window action)
315 if (this->is_client_running()) {
316 this->client_->cancel_pairing_window();
317 }
318}
319
320const char *SendspinHub::get_mac_address_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
321 // The server matches this MAC against the L2 source MAC of the device's multicast traffic.
322 // ESP-IDF derives the ethernet MAC as base+3 by default on ESP32-S3, so we cannot use the
323 // eFuse base MAC when ethernet is the active interface.
324#ifdef USE_ETHERNET
325 if (ethernet::global_eth_component != nullptr) {
327 } else {
329 }
330#else
332#endif
333 // The pretty format is uppercase, but SendspinClientConfig::mac_address must be lowercase.
334 for (char &c : buf) {
335 if (c >= 'A' && c <= 'F') {
336 c += 'a' - 'A';
337 }
338 }
339 return buf.data();
340}
341
342// Validation rejects a static code together with the dynamic code, so at most one is set.
345 return LOG_STR_LITERAL("dynamic");
346 }
347 if (this->static_pairing_code_ != nullptr) {
348 return LOG_STR_LITERAL("static");
349 }
350 return LOG_STR_LITERAL("none");
351}
352
354 return this->model_ != nullptr ? this->model_ : App.get_name().c_str();
355}
356
357sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
358 sendspin::SendspinClientConfig config;
359
360 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
361 config.mac_address = SendspinHub::get_mac_address_into_buffer(mac_buf);
362 config.name = App.get_friendly_name();
363 config.product_name = this->get_product_name_();
364 config.manufacturer = this->manufacturer_;
365 config.software_version = this->firmware_version_;
366 config.httpd_psram_stack = this->task_stack_in_psram_;
367 config.protocol_task_psram_stack = this->task_stack_in_psram_;
368 config.max_pairing_records = SENDSPIN_RECORD_SLOTS;
369
370 // The dynamic code needs a channel and a format. Only digits, since automations get the bare string and could not
371 // tell a QR code token apart.
373 config.pairing_code_out_channels = {sendspin::SendspinPairingCodeChannel::DISPLAY};
374 config.pairing_code_formats = {sendspin::SendspinPairingCodeFormat::DIGITS};
375 }
376
377 if (this->static_pairing_code_ != nullptr) {
378 config.static_pairing_code_locations = {"operator"};
379 config.static_pairing_code = this->static_pairing_code_;
380 }
381
382 return config;
383}
384
385// --- SendspinClientListener overrides ---
386// THREAD CONTEXT: Main loop (fired from client_->loop())
387
388void SendspinHub::on_group_update(const sendspin::GroupUpdateObject &group) {
389 this->group_update_callbacks_.call(group);
390}
391
400
409
411
413
414// Only digits are offered, and the library refuses an activation in any other format.
415void SendspinHub::on_display_pairing_code(const std::string &code, sendspin::SendspinPairingCodeFormat /*format*/) {
416 this->display_pairing_code_callbacks_.call(code);
417}
418
420
421void SendspinHub::on_pairing_succeeded(const std::string &server_id) {
422 this->pairing_succeeded_callbacks_.call(server_id);
423}
424
425void SendspinHub::on_pairing_failed(const std::string &server_id, sendspin::SendspinPairAbortReason reason) {
426 this->pairing_failed_callbacks_.call(server_id, pair_abort_reason_to_string(reason));
427}
428
429// --- SendspinNetworkProvider override ---
430
431// THREAD CONTEXT: Main loop (polled by start() and client_->loop())
433
434// --- SendspinPersistenceProvider overrides ---
435// THREAD CONTEXT: Main loop (the library makes every provider call there, including from start() and stop())
436
437std::pair<ESPPreferenceObject *, size_t> SendspinHub::pref_for_key_(const std::string &key) {
438 namespace keys = sendspin::persistence_keys;
439 if (auto slot = parse_record_slot(key); slot.has_value()) {
440 return {&this->record_slot_prefs_[*slot], keys::RECORD_SLOT_SIZE};
441 }
442 if (key == keys::RECORD_ORDER) {
444 }
445 if (key == keys::KEYPAIR) {
446 return {&this->keypair_pref_, keys::KEYPAIR_SIZE};
447 }
448 if (key == keys::PAIRING_PSK) {
449 return {&this->pairing_psk_pref_, keys::PAIRING_PSK_SIZE};
450 }
451 if (key == keys::LAST_PLAYED) {
452 return {&this->last_played_pref_, keys::LAST_PLAYED_SIZE};
453 }
454 if (key == keys::OUTPUT_DELAY) {
455 return {&this->output_delay_pref_, keys::OUTPUT_DELAY_SIZE};
456 }
457 return {nullptr, 0};
458}
459
460std::optional<std::vector<uint8_t>> SendspinHub::load_blob(const std::string &key) {
461 auto [pref, size] = this->pref_for_key_(key);
462 if (pref == nullptr) {
463 ESP_LOGW(TAG, "load_blob: unknown key \"%s\"", key.c_str());
464 return std::nullopt;
465 }
466 return read_blob(*pref, size);
467}
468
469bool SendspinHub::save_blob(const std::string &key, const uint8_t *data, size_t len) {
470 auto [pref, size] = this->pref_for_key_(key);
471 if (pref == nullptr) {
472 ESP_LOGW(TAG, "save_blob: unknown key \"%s\"", key.c_str());
473 return false;
474 }
475 return write_blob(*pref, key.c_str(), size, data, len);
476}
477
478// Writes every component's queued preferences, so a failure may belong to another component.
480
481// --- Sendspin role specific methods/overrides ---
482
483#ifdef USE_SENDSPIN_ARTWORK
484// THREAD CONTEXT: Dedicated artwork decode thread; downstream callbacks run here too
485void SendspinHub::on_image_decode(uint8_t slot, const uint8_t *data, size_t length,
486 sendspin::SendspinImageFormat format) {
487 this->artwork_image_decode_callbacks_.call(slot, data, length, format);
488}
489
490// THREAD CONTEXT: Main loop (fired from client_->loop() once the slot's offset-shifted display
491// deadline is reached; lateness_ms reports how far past the deadline the display slipped)
492void SendspinHub::on_image_display(uint8_t slot, uint32_t lateness_ms) {
493 this->artwork_image_display_callbacks_.call(slot, lateness_ms);
494}
495
496// THREAD CONTEXT: Main loop (fired from client_->loop())
497void SendspinHub::on_image_clear(uint8_t slot) { this->artwork_image_clear_callbacks_.call(slot); }
498
499// THREAD CONTEXT: Main loop (invoked from SendspinImageSlot once a delivery is fully presented)
501 if (this->artwork_role_ != nullptr) {
502 this->artwork_role_->frame_done(slot);
503 }
504}
505#endif
506
507#ifdef USE_SENDSPIN_CONTROLLER
508// THREAD CONTEXT: Main loop (invoked from ESPHome actions / other components)
509void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional<uint8_t> volume,
510 std::optional<bool> mute) {
511 if (this->is_client_running()) {
512 sendspin::ClientCommandControllerObject obj = {
513 .command = command,
514 .volume = volume,
515 .muted = mute,
516 };
517 this->controller_role_->send_command(obj);
518 }
519}
520
521// THREAD CONTEXT: Main loop (invoked from the sendspin.switch action)
522void SendspinHub::switch_client() { this->send_client_command(sendspin::SendspinControllerCommand::SWITCH); }
523
524// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop())
525void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) {
526 this->controller_state_callbacks_.call(state);
527}
528
529// THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop())
530// Unlike metadata, this cannot be fanned out as a default-constructed state object: volume and muted are plain values
531// rather than optionals, so children would read a real-looking 0% volume where we mean no value at all. A separate
532// callback lets each child clear only what it can represent.
534#endif
535
536#ifdef USE_SENDSPIN_METADATA
537// THREAD CONTEXT: Main loop (MetadataRoleListener override, fired from client_->loop())
538void SendspinHub::on_metadata(const sendspin::ServerMetadataStateObject &metadata) {
539 this->metadata_update_callbacks_.call(metadata);
540}
541
542// THREAD CONTEXT: Main loop (MetadataRoleListener override, fired from client_->loop())
543// The cached metadata was dropped because the connection to the server was lost, so what the children now mirror is
544// the empty state. Fanning that out as a default-constructed state object rather than through a separate callback
545// keeps one code path in the children: every field is nullopt, which they already publish as empty/unknown.
546void SendspinHub::on_metadata_clear() { this->metadata_update_callbacks_.call(sendspin::ServerMetadataStateObject{}); }
547
548// THREAD CONTEXT: Main loop (invoked from Sendspin components)
550 if (this->is_ready()) {
551 return this->metadata_role_->get_track_progress_ms();
552 }
553 return 0;
554}
555#endif
556
557#ifdef USE_SENDSPIN_PLAYER
558// THREAD CONTEXT: Main loop, called from child component setup() after player role is created and configured
559sendspin::PlayerRole *SendspinHub::get_player_role() {
560 if (this->is_ready()) {
561 return this->client_->player();
562 }
563 return nullptr;
564}
565#endif
566
567} // namespace esphome::sendspin_
568
569#endif // USE_ESP_IDF
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
const StringRef & get_friendly_name() const
Get the friendly name of this Application set by pre_setup().
bool is_ready() const
bool status_has_error() const
Definition component.h:282
StringRef is a reference to a string owned by something else.
Definition string_ref.h:31
constexpr const char * c_str() const
Definition string_ref.h:78
static constexpr StringRef from_lit(const CharT(&s)[N])
Definition string_ref.h:55
const char * get_eth_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
bool set_service_enabled(const char *service_type, const char *proto, bool enabled)
Enable or disable a compiled-in service, matched by type and proto (e.g.
Thin adapter over sendspin::SendspinClient.
ESPPreferenceObject output_delay_pref_
CallbackManager< void(uint8_t, const uint8_t *, size_t, sendspin::SendspinImageFormat)> artwork_image_decode_callbacks_
CallbackManager< void(uint8_t, uint32_t)> artwork_image_display_callbacks_
sendspin::PlayerRoleListener * player_listener_
const char * get_product_name_() const
Returns the product name reported to the server: the configured model, or the device name.
std::optional< bool > unpaired_access_
void on_controller_state_clear() override
std::optional< std::vector< uint8_t > > load_blob(const std::string &key) override
void confirm_pairing_window()
Confirms a pairing attempt on the device.
CallbackManager< void(const sendspin::ServerMetadataStateObject &)> metadata_update_callbacks_
ESPPreferenceObject record_order_pref_
std::optional< std::string > get_pairing_token() const
Returns the pairing token a Sendspin server uses to pair with this device.
LazyCallbackManager< void(const std::string &, StringRef)> pairing_failed_callbacks_
void set_unpaired_access_enabled(bool enabled)
Turns unpaired (Sentinel) access on or off.
LazyCallbackManager< void(const sendspin::ServerStateControllerObject &)> controller_state_callbacks_
void on_release_high_performance() override
bool save_blob(const std::string &key, const uint8_t *data, size_t len) override
LazyCallbackManager< void()> clear_pairing_code_callbacks_
sendspin::MetadataRole * metadata_role_
void on_display_pairing_code(const std::string &code, sendspin::SendspinPairingCodeFormat format) override
bool is_client_running() const
Returns whether the Sendspin client is running.
const char * pairing_code_method_() const
CallbackManager< void(const sendspin::GroupUpdateObject &)> group_update_callbacks_
LazyCallbackManager< void(const std::string &)> display_pairing_code_callbacks_
void switch_client()
Sends the SWITCH controller command; exposed as the sendspin.switch action.
void leave_group()
Leaves the current group so another source can use the speaker.
sendspin::PlayerRoleConfig player_config_
void on_metadata(const sendspin::ServerMetadataStateObject &metadata) override
void cancel_pairing_window()
Closes an open pairing window, so a waiting pairing attempt is not confirmed.
ESPPreferenceObject last_played_pref_
LazyCallbackManager< void(const std::string &)> pairing_succeeded_callbacks_
static const char * get_mac_address_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
Writes the active network interface's MAC, in lowercase, into buf and returns its data pointer.
sendspin::PlayerRole * get_player_role()
Child components call this to get the PlayerRole instance after setup, so they can push updates to it...
void on_image_clear(uint8_t slot) override
void on_pairing_failed(const std::string &server_id, sendspin::SendspinPairAbortReason reason) override
void disconnect_from_server(sendspin::SendspinGoodbyeReason reason)
Disconnects the underlying client from the current server.
std::unique_ptr< sendspin::SendspinClient > client_
sendspin::ArtworkRoleConfig artwork_config_
void update_mdns_service_()
Keeps the _sendspin mDNS service advertised while the client is running.
sendspin::SendspinClientConfig build_client_config_()
Builds the SendspinClientConfig from ESPHome configuration and platform info.
mdns::MDNSComponent * mdns_
sendspin::ControllerRole * controller_role_
void on_pairing_succeeded(const std::string &server_id) override
uint32_t get_track_progress_ms() const
Returns the interpolated track progress in milliseconds, or 0 if the hub is not yet ready.
std::optional< bool > enabled_
StringRef get_client_id() const
Returns the Sendspin client_id: base64url of the client's static public key.
void send_client_command(sendspin::SendspinControllerCommand command, std::optional< uint8_t > volume=std::nullopt, std::optional< bool > mute=std::nullopt)
LazyCallbackManager< void()> controller_state_clear_callbacks_
CallbackManager< void(uint8_t)> artwork_image_clear_callbacks_
ESPPreferenceObject keypair_pref_
LazyCallbackManager< void()> open_pairing_window_callbacks_
sendspin::ArtworkRole * artwork_role_
void on_request_high_performance() override
std::pair< ESPPreferenceObject *, size_t > pref_for_key_(const std::string &key)
The preference a library key is stored in and its blob size, or {nullptr, 0} for a key with no storag...
void set_enabled(bool enabled)
Requests the Sendspin client, including the server, the roles and the mDNS advertisement,...
std::array< ESPPreferenceObject, SENDSPIN_RECORD_SLOTS > record_slot_prefs_
void on_image_display(uint8_t slot, uint32_t lateness_ms) override
void on_image_decode(uint8_t slot, const uint8_t *data, size_t length, sendspin::SendspinImageFormat format) override
ESPPreferenceObject pairing_psk_pref_
void on_controller_state(const sendspin::ServerStateControllerObject &state) override
LazyCallbackManager< void()> close_pairing_window_callbacks_
void connect_to_server(const std::string &url)
Connects the underlying client to the given Sendspin server.
void on_group_update(const sendspin::GroupUpdateObject &group) override
void artwork_frame_done(uint8_t slot)
Acknowledges the most recent artwork delivery (display or clear) for a slot.
bool request_high_performance()
Request high-performance mode (no power saving) for improved WiFi latency.
void release_roaming_suppression()
Release a roaming suppression request.
void request_roaming_suppression()
Request that post-connect roaming scans be suppressed.
bool release_high_performance()
Release a high-performance mode request.
bool state
Definition fan.h:2
std::vector< uint8_t > & out
Definition gif.cpp:49
const char * format
EthernetComponent * global_eth_component
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:36
constexpr size_t SENDSPIN_RECORD_SLOTS
Pairing-record slots, handed to the client as max_pairing_records.
SendspinHub * global_sendspin_hub
ESPHOME_LOG_TAG(TAG, "sendspin.image")
WiFiComponent * global_wifi_component
const void size_t len
Definition hal.h:68
uint16_t size
Definition helpers.cpp:34
ESPPreferences * global_preferences
const char * get_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
Get the device MAC address into the given buffer, in colon-separated uppercase hex notation.
Definition helpers.cpp:854
Application App
Global storage of Application pointer - only one Application can exist.
constexpr uint32_t fnv1a_hash(const char *str)
Calculate a FNV-1a hash of str.
Definition helpers.h:955
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
bool sync()
Commit pending writes to flash.
Definition preferences.h:33
uint16_t length
Definition tt21100.cpp:0