#include "sendspin_hub.h" #ifdef USE_ESP32 #include "esphome/components/network/util.h" #ifdef USE_ETHERNET #include "esphome/components/ethernet/ethernet_component.h" #endif #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" #endif #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/version.h" #include namespace esphome::sendspin_ { static const char *const TAG = "sendspin.hub"; void SendspinHub::setup() { auto config = this->build_client_config_(); this->client_ = std::make_unique(std::move(config)); // Set up persistence (preferences must be initialized before providers are added to the client) this->last_played_server_pref_ = global_preferences->make_preference(fnv1a_hash("sendspin_last_played")); #ifdef USE_SENDSPIN_PLAYER this->static_delay_pref_ = global_preferences->make_preference(fnv1a_hash("sendspin_static_delay")); #endif // Wire providers and client listener this->client_->set_listener(this); this->client_->set_network_provider(this); this->client_->set_persistence_provider(this); #ifdef USE_SENDSPIN_CONTROLLER this->controller_role_ = &this->client_->add_controller(); this->controller_role_->set_listener(this); #endif #ifdef USE_SENDSPIN_METADATA this->metadata_role_ = &this->client_->add_metadata(); this->metadata_role_->set_listener(this); #endif #ifdef USE_SENDSPIN_PLAYER this->client_->add_player(this->player_config_).set_listener(this->player_listener_); #endif if (!this->client_->start_server()) { ESP_LOGE(TAG, "Failed to start Sendspin server"); this->mark_failed(); return; } } void SendspinHub::loop() { this->client_->loop(); } void SendspinHub::dump_config() { char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "Sendspin Hub:\n" " Client ID: %s\n" " Task stack in PSRAM: %s", get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_)); } // --- Delegating methods --- // THREAD CONTEXT: Main loop (invoked from Sendspin components) void SendspinHub::connect_to_server(const std::string &url) { if (this->is_ready()) { this->client_->connect_to(url); } } // THREAD CONTEXT: Main loop (invoked from Sendspin components) void SendspinHub::disconnect_from_server(sendspin::SendspinGoodbyeReason reason) { if (this->is_ready()) { this->client_->disconnect(reason); } } // THREAD CONTEXT: Main loop (invoked from Sendspin components) void SendspinHub::update_state(sendspin::SendspinClientState state) { if (this->is_ready()) { this->client_->update_state(state); } } const char *SendspinHub::get_client_id_into_buffer(std::span buf) { // The server matches client_id against the L2 source MAC of the device's multicast traffic. // ESP-IDF derives the ethernet MAC as base+3 by default on ESP32-S3, so we cannot use the // eFuse base MAC when ethernet is the active interface. #ifdef USE_ETHERNET if (ethernet::global_eth_component != nullptr) { return ethernet::global_eth_component->get_eth_mac_address_pretty_into_buffer(buf); } #endif return get_mac_address_pretty_into_buffer(buf); } sendspin::SendspinClientConfig SendspinHub::build_client_config_() { sendspin::SendspinClientConfig config; char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf); config.name = App.get_friendly_name(); config.product_name = App.get_name(); config.manufacturer = "ESPHome"; config.software_version = ESPHOME_VERSION; config.httpd_psram_stack = this->task_stack_in_psram_; return config; } // --- SendspinClientListener overrides --- // THREAD CONTEXT: Main loop (fired from client_->loop()) void SendspinHub::on_group_update(const sendspin::GroupUpdateObject &group) { this->group_update_callbacks_.call(group); } void SendspinHub::on_request_high_performance() { #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) { wifi::global_wifi_component->request_high_performance(); } #endif } void SendspinHub::on_release_high_performance() { #ifdef USE_WIFI if (wifi::global_wifi_component != nullptr) { wifi::global_wifi_component->release_high_performance(); } #endif } // --- SendspinNetworkProvider override --- // THREAD CONTEXT: Main loop (polled by client_->loop()) bool SendspinHub::is_network_ready() { return network::is_connected(); } // --- SendspinPersistenceProvider overrides --- // THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events) bool SendspinHub::save_last_server_hash(uint32_t hash) { LastPlayedServerPref pref{.server_id_hash = hash}; bool ok = this->last_played_server_pref_.save(&pref); if (ok) { ESP_LOGD(TAG, "Persisted last played server hash: 0x%08" PRIX32, hash); } else { ESP_LOGW(TAG, "Failed to persist last played server hash"); } return ok; } // THREAD CONTEXT: Main loop (invoked by client_->loop() during lifecycle events) std::optional SendspinHub::load_last_server_hash() { LastPlayedServerPref pref{}; if (this->last_played_server_pref_.load(&pref)) { ESP_LOGI(TAG, "Loaded last played server hash: 0x%08" PRIX32, pref.server_id_hash); return pref.server_id_hash; } return std::nullopt; } // --- Sendspin role specific methods/overrides --- #ifdef USE_SENDSPIN_CONTROLLER // THREAD CONTEXT: Main loop (invoked from ESPHome actions / other components) void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional volume, std::optional mute) { if (this->is_ready()) { this->controller_role_->send_command(command, volume, mute); } } // THREAD CONTEXT: Main loop (ControllerRoleListener override, fired from client_->loop()) void SendspinHub::on_controller_state(const sendspin::ServerStateControllerObject &state) { this->controller_state_callbacks_.call(state); } #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); } // 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 // THREAD CONTEXT: Main loop, called from child component setup() after player role is created and configured sendspin::PlayerRole *SendspinHub::get_player_role() { if (this->is_ready()) { return this->client_->player(); } return nullptr; } // THREAD CONTEXT: Main loop (SendspinPersistenceProvider override) bool SendspinHub::save_static_delay(uint16_t delay_ms) { StaticDelayPref pref{.delay_ms = delay_ms}; bool ok = this->static_delay_pref_.save(&pref); if (ok) { ESP_LOGD(TAG, "Persisted static delay: %u ms", delay_ms); } else { ESP_LOGW(TAG, "Failed to persist static delay"); } return ok; } // THREAD CONTEXT: Main loop (SendspinPersistenceProvider override) std::optional SendspinHub::load_static_delay() { StaticDelayPref pref{}; if (this->static_delay_pref_.load(&pref)) { ESP_LOGI(TAG, "Loaded static delay: %u ms", pref.delay_ms); return pref.delay_ms; } return std::nullopt; } #endif } // namespace esphome::sendspin_ #endif // USE_ESP32