1use std::any::{Any, TypeId};
29use std::collections::{HashMap, HashSet};
30use std::net::IpAddr;
31use std::sync::{mpsc, Arc, Mutex, OnceLock};
32use std::thread::JoinHandle;
33use std::time::{Duration, Instant};
34
35use parking_lot::RwLock;
36
37use sonos_api::{Service, ServiceScope};
38use sonos_discovery::Device;
39use sonos_event_manager::{SonosEventManager, WatchRegistry};
40use tracing::info;
41
42use crate::event_worker::spawn_state_event_worker;
43use crate::iter::ChangeIterator;
44use crate::model::{GroupId, SpeakerId, SpeakerInfo};
45use crate::property::{GroupInfo, Property, Scope, SonosProperty, Topology};
46use crate::{Result, StateError};
47
48pub type EventInitFn = Arc<
54 dyn Fn() -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> + Send + Sync,
55>;
56
57#[derive(Debug, Clone)]
63pub struct ChangeEvent {
64 pub speaker_id: SpeakerId,
66 pub property_key: &'static str,
68 pub service: Service,
70 pub timestamp: Instant,
72}
73
74impl ChangeEvent {
75 pub fn new(speaker_id: SpeakerId, property_key: &'static str, service: Service) -> Self {
76 Self {
77 speaker_id,
78 property_key,
79 service,
80 timestamp: Instant::now(),
81 }
82 }
83}
84
85#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
117pub(crate) struct WatchHolds {
118 subscription: bool,
120 direct: usize,
122}
123
124impl WatchHolds {
125 fn is_held(&self) -> bool {
126 self.subscription || self.direct > 0
127 }
128}
129
130pub(crate) type WatchCounts = HashMap<(SpeakerId, &'static str), WatchHolds>;
132
133fn retain_subscription_watch(
135 watched: &RwLock<WatchCounts>,
136 speaker_id: &SpeakerId,
137 key: &'static str,
138) {
139 watched
140 .write()
141 .entry((speaker_id.clone(), key))
142 .or_default()
143 .subscription = true;
144}
145
146pub(crate) fn retain_direct_watch(
148 watched: &RwLock<WatchCounts>,
149 speaker_id: &SpeakerId,
150 key: &'static str,
151) {
152 watched
153 .write()
154 .entry((speaker_id.clone(), key))
155 .or_default()
156 .direct += 1;
157}
158
159fn release_direct_watch(watched: &RwLock<WatchCounts>, speaker_id: &SpeakerId, key: &'static str) {
164 let mut guard = watched.write();
165 let entry_key = (speaker_id.clone(), key);
166 if let Some(holds) = guard.get_mut(&entry_key) {
167 holds.direct = holds.direct.saturating_sub(1);
168 if !holds.is_held() {
169 guard.remove(&entry_key);
170 }
171 }
172}
173
174fn release_subscription_watch(
180 watched: &RwLock<WatchCounts>,
181 speaker_id: &SpeakerId,
182 key: &'static str,
183) {
184 let mut guard = watched.write();
185 let entry_key = (speaker_id.clone(), key);
186 if let Some(holds) = guard.get_mut(&entry_key) {
187 holds.subscription = false;
188 if !holds.is_held() {
189 guard.remove(&entry_key);
190 }
191 }
192}
193
194pub(crate) fn is_pair_watched(
196 watched: &WatchCounts,
197 speaker_id: &SpeakerId,
198 key: &'static str,
199) -> bool {
200 watched.contains_key(&(speaker_id.clone(), key))
201}
202
203pub struct StateStore {
209 pub(crate) speakers: HashMap<SpeakerId, SpeakerInfo>,
211 pub(crate) ip_to_speaker: HashMap<IpAddr, SpeakerId>,
213 pub(crate) speaker_props: HashMap<SpeakerId, PropertyBag>,
215 pub(crate) groups: HashMap<GroupId, GroupInfo>,
217 pub(crate) group_props: HashMap<GroupId, PropertyBag>,
219 pub(crate) system_props: PropertyBag,
221 pub(crate) speaker_to_group: HashMap<SpeakerId, GroupId>,
223 pub(crate) satellite_ids: HashSet<SpeakerId>,
225}
226
227impl StateStore {
228 pub(crate) fn new() -> Self {
229 Self {
230 speakers: HashMap::new(),
231 ip_to_speaker: HashMap::new(),
232 speaker_props: HashMap::new(),
233 groups: HashMap::new(),
234 group_props: HashMap::new(),
235 system_props: PropertyBag::new(),
236 speaker_to_group: HashMap::new(),
237 satellite_ids: HashSet::new(),
238 }
239 }
240
241 pub(crate) fn add_speaker(&mut self, speaker: SpeakerInfo) {
242 let id = speaker.id.clone();
243 let ip = speaker.ip_address;
244 self.ip_to_speaker.insert(ip, id.clone());
245 self.speakers.insert(id.clone(), speaker);
246 self.speaker_props
247 .entry(id)
248 .or_insert_with(PropertyBag::new);
249 }
250
251 fn speaker(&self, id: &SpeakerId) -> Option<&SpeakerInfo> {
252 self.speakers.get(id)
253 }
254
255 fn speakers(&self) -> Vec<SpeakerInfo> {
256 self.speakers.values().cloned().collect()
257 }
258
259 pub(crate) fn add_group(&mut self, group: GroupInfo) {
260 let id = group.id.clone();
261 for member_id in &group.member_ids {
263 self.speaker_to_group.insert(member_id.clone(), id.clone());
264 }
265 self.groups.insert(id.clone(), group);
266 self.group_props.entry(id).or_insert_with(PropertyBag::new);
267 }
268
269 #[allow(dead_code)]
271 pub(crate) fn get_group_for_speaker(&self, speaker_id: &SpeakerId) -> Option<&GroupInfo> {
272 let group_id = self.speaker_to_group.get(speaker_id)?;
273 self.groups.get(group_id)
274 }
275
276 pub(crate) fn clear_groups(&mut self) {
280 self.groups.clear();
281 self.group_props.clear();
282 self.speaker_to_group.clear();
283 }
284
285 pub(crate) fn resolve_coordinator(&self, speaker_id: &SpeakerId) -> SpeakerId {
290 self.speaker_to_group
291 .get(speaker_id)
292 .and_then(|gid| self.groups.get(gid))
293 .map(|group| group.coordinator_id.clone())
294 .unwrap_or_else(|| speaker_id.clone())
295 }
296
297 pub(crate) fn get_resolved<P: SonosProperty>(&self, speaker_id: &SpeakerId) -> Option<P> {
307 if P::SERVICE.scope() == ServiceScope::PerCoordinator && P::SCOPE == Scope::Speaker {
308 let coordinator_id = self.resolve_coordinator(speaker_id);
309 self.speaker_props.get(&coordinator_id)?.get::<P>()
310 } else {
311 self.speaker_props.get(speaker_id)?.get::<P>()
312 }
313 }
314
315 pub(crate) fn resolve_write_target<P: SonosProperty>(
321 &self,
322 speaker_id: &SpeakerId,
323 ) -> SpeakerId {
324 if P::SERVICE.scope() == ServiceScope::PerCoordinator && P::SCOPE == Scope::Speaker {
325 self.resolve_coordinator(speaker_id)
326 } else {
327 speaker_id.clone()
328 }
329 }
330
331 #[cfg_attr(not(test), allow(dead_code))]
332 pub(crate) fn get<P: Property>(&self, speaker_id: &SpeakerId) -> Option<P> {
333 self.speaker_props.get(speaker_id)?.get::<P>()
334 }
335
336 pub(crate) fn set<P: Property>(&mut self, speaker_id: &SpeakerId, value: P) -> bool {
337 let bag = self
338 .speaker_props
339 .entry(speaker_id.clone())
340 .or_insert_with(PropertyBag::new);
341 bag.set(value)
342 }
343
344 pub(crate) fn get_group<P: Property>(&self, group_id: &GroupId) -> Option<P> {
345 self.group_props.get(group_id)?.get::<P>()
346 }
347
348 pub(crate) fn set_group<P: Property>(&mut self, group_id: &GroupId, value: P) -> bool {
349 let bag = self
350 .group_props
351 .entry(group_id.clone())
352 .or_insert_with(PropertyBag::new);
353 bag.set(value)
354 }
355
356 fn set_system<P: Property>(&mut self, value: P) -> bool {
357 self.system_props.set(value)
358 }
359
360 pub(crate) fn update_speaker_ip_address(
362 &mut self,
363 speaker_id: &SpeakerId,
364 new_ip: IpAddr,
365 ) -> Option<IpAddr> {
366 if let Some(info) = self.speakers.get_mut(speaker_id) {
367 let old_ip = info.ip_address;
368 if old_ip != new_ip {
369 info.ip_address = new_ip;
370 return Some(old_ip);
371 }
372 }
373 None
374 }
375
376 fn is_empty(&self) -> bool {
377 self.speakers.is_empty()
378 }
379
380 fn speaker_count(&self) -> usize {
381 self.speakers.len()
382 }
383
384 fn group_count(&self) -> usize {
385 self.groups.len()
386 }
387}
388
389pub(crate) struct PropertyBag {
394 values: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
396}
397
398impl PropertyBag {
399 pub(crate) fn new() -> Self {
400 Self {
401 values: HashMap::new(),
402 }
403 }
404
405 fn get<P: Property>(&self) -> Option<P> {
406 let type_id = TypeId::of::<P>();
407 self.values
408 .get(&type_id)
409 .and_then(|boxed| boxed.downcast_ref::<P>())
410 .cloned()
411 }
412
413 fn set<P: Property>(&mut self, value: P) -> bool {
414 let type_id = TypeId::of::<P>();
415 let current = self
416 .values
417 .get(&type_id)
418 .and_then(|boxed| boxed.downcast_ref::<P>());
419
420 if current != Some(&value) {
421 self.values.insert(type_id, Box::new(value));
422 true
423 } else {
424 false
425 }
426 }
427}
428
429pub struct StateManager {
438 store: Arc<RwLock<StateStore>>,
440
441 watched: Arc<RwLock<WatchCounts>>,
451
452 ip_to_speaker: Arc<RwLock<HashMap<IpAddr, SpeakerId>>>,
454
455 event_manager: OnceLock<Arc<SonosEventManager>>,
457
458 event_tx: mpsc::Sender<ChangeEvent>,
460
461 event_rx: Arc<Mutex<mpsc::Receiver<ChangeEvent>>>,
463
464 _worker: Mutex<Option<JoinHandle<()>>>,
466
467 cleanup_timeout: Duration,
469
470 key_to_service: Arc<RwLock<HashMap<&'static str, Service>>>,
473
474 event_init: OnceLock<EventInitFn>,
477}
478
479struct StateWatchRegistry {
489 watched: Arc<RwLock<WatchCounts>>,
490 ip_to_speaker: Arc<RwLock<HashMap<IpAddr, SpeakerId>>>,
491 key_to_service: Arc<RwLock<HashMap<&'static str, Service>>>,
492}
493
494impl WatchRegistry for StateWatchRegistry {
495 fn register_watch(&self, speaker_id: &SpeakerId, key: &'static str, service: Service) {
496 retain_subscription_watch(&self.watched, speaker_id, key);
497 self.key_to_service.write().insert(key, service);
498 }
499
500 fn unregister_watches_for_service(&self, ip: IpAddr, service: Service) {
501 let speaker_id = match self.ip_to_speaker.read().get(&ip).cloned() {
503 Some(id) => id,
504 None => {
505 tracing::warn!(
506 "unregister_watches_for_service: no speaker found for IP {}",
507 ip
508 );
509 return;
510 }
511 };
512
513 let service_keys: Vec<&'static str> = self
515 .key_to_service
516 .read()
517 .iter()
518 .filter(|(_, &svc)| svc == service)
519 .map(|(&key, _)| key)
520 .collect();
521
522 for key in service_keys {
529 release_subscription_watch(&self.watched, &speaker_id, key);
530 }
531 }
532}
533
534impl StateManager {
535 pub fn new() -> Result<Self> {
543 Self::builder().build()
544 }
545
546 pub fn builder() -> StateManagerBuilder {
548 StateManagerBuilder::default()
549 }
550
551 pub fn add_devices(&self, devices: Vec<Device>) -> Result<()> {
560 let mut store = self.store.write();
561 let mut ip_map = self.ip_to_speaker.write();
562
563 for device in devices {
564 let speaker_id = SpeakerId::new(&device.id);
565 let ip: IpAddr = device
566 .ip_address
567 .parse()
568 .map_err(|_| StateError::InvalidIpAddress(device.ip_address.clone()))?;
569
570 let friendly_name = if device.room_name.is_empty() || device.room_name == "Unknown" {
571 device.name.clone()
572 } else {
573 device.room_name.clone()
574 };
575
576 let info = SpeakerInfo {
577 id: speaker_id.clone(),
578 name: friendly_name,
579 room_name: device.room_name.clone(),
580 ip_address: ip,
581 port: device.port,
582 model_name: device.model_name.clone(),
583 software_version: "unknown".to_string(),
584 boot_seq: 0,
585 satellites: vec![],
586 };
587
588 ip_map.insert(ip, speaker_id.clone());
590 tracing::debug!(
591 "Added speaker {} at IP {} to ip_to_speaker map",
592 speaker_id.as_str(),
593 ip
594 );
595
596 store.add_speaker(info);
597 }
598
599 drop(store);
601 drop(ip_map);
602
603 if let Some(em) = self.event_manager.get() {
604 let devices_for_em: Vec<_> = self
605 .speaker_infos()
606 .iter()
607 .map(|info| sonos_discovery::Device {
608 id: info.id.as_str().to_string(),
609 name: info.name.clone(),
610 room_name: info.room_name.clone(),
611 ip_address: info.ip_address.to_string(),
612 port: info.port,
613 model_name: info.model_name.clone(),
614 })
615 .collect();
616
617 if let Err(e) = em.add_devices(devices_for_em) {
618 tracing::warn!("Failed to add devices to event manager: {}", e);
619 }
620 }
621
622 Ok(())
623 }
624
625 pub fn speaker_infos(&self) -> Vec<SpeakerInfo> {
627 self.store.read().speakers()
628 }
629
630 pub fn speaker_info(&self, speaker_id: &SpeakerId) -> Option<SpeakerInfo> {
632 self.store.read().speaker(speaker_id).cloned()
633 }
634
635 pub fn get_speaker_ip(&self, speaker_id: &SpeakerId) -> Option<IpAddr> {
637 self.store.read().speaker(speaker_id).map(|s| s.ip_address)
638 }
639
640 pub fn get_boot_seq(&self, speaker_id: &SpeakerId) -> Option<u32> {
642 self.store.read().speaker(speaker_id).map(|s| s.boot_seq)
643 }
644
645 pub fn update_speaker_ip(&self, speaker_id: &SpeakerId, new_ip: IpAddr) {
647 let old_ip = {
648 let mut store = self.store.write();
649 store.update_speaker_ip_address(speaker_id, new_ip)
650 };
651 if let Some(old_ip) = old_ip {
652 let mut map = self.ip_to_speaker.write();
653 map.remove(&old_ip);
654 map.insert(new_ip, speaker_id.clone());
655 }
656 }
657
658 pub fn get_satellite_ids(&self) -> Vec<SpeakerId> {
660 self.store.read().satellite_ids.iter().cloned().collect()
661 }
662
663 pub fn set_satellite_ids(&self, ids: Vec<SpeakerId>) {
665 self.store.write().satellite_ids = ids.into_iter().collect();
666 }
667
668 pub fn iter(&self) -> ChangeIterator {
684 ChangeIterator::new(Arc::clone(&self.event_rx))
685 }
686
687 pub fn get_property<P: SonosProperty>(&self, speaker_id: &SpeakerId) -> Option<P> {
692 self.store.read().get_resolved::<P>(speaker_id)
693 }
694
695 pub fn get_group_property<P: Property>(&self, group_id: &GroupId) -> Option<P> {
697 self.store.read().get_group::<P>(group_id)
698 }
699
700 pub fn set_property<P: SonosProperty>(&self, speaker_id: &SpeakerId, value: P) {
717 let (target_id, changed) = {
721 let mut store = self.store.write();
722 let target_id = store.resolve_write_target::<P>(speaker_id);
723 let changed = store.set::<P>(&target_id, value);
724 (target_id, changed)
725 };
726
727 if changed {
728 self.maybe_emit_change(speaker_id, P::KEY, P::SERVICE);
731 if target_id != *speaker_id {
734 self.maybe_emit_change(&target_id, P::KEY, P::SERVICE);
735 }
736 }
737 }
738
739 pub fn set_group_property<P: SonosProperty>(&self, group_id: &GroupId, value: P) {
745 let coordinator_id = {
746 let mut store = self.store.write();
747 let changed = store.set_group::<P>(group_id, value);
748 if !changed {
749 return;
750 }
751 store.groups.get(group_id).map(|g| g.coordinator_id.clone())
752 };
753
754 if let Some(coordinator_id) = coordinator_id {
755 self.maybe_emit_change(&coordinator_id, P::KEY, P::SERVICE);
756 }
757 }
758
759 pub fn register_watch(&self, speaker_id: &SpeakerId, property_key: &'static str) {
764 retain_direct_watch(&self.watched, speaker_id, property_key);
765 }
766
767 pub fn unregister_watch(&self, speaker_id: &SpeakerId, property_key: &'static str) {
774 release_direct_watch(&self.watched, speaker_id, property_key);
775 }
776
777 pub fn watch_property_with_subscription<P: SonosProperty>(
785 &self,
786 speaker_id: &SpeakerId,
787 ) -> Result<Option<P>> {
788 self.register_watch(speaker_id, P::KEY);
790
791 if let Some(em) = self.event_manager.get() {
793 if let Some(ip) = self.get_speaker_ip(speaker_id) {
795 if let Err(e) = em.ensure_service_subscribed(ip, P::SERVICE) {
796 tracing::warn!(
797 "Failed to subscribe to {:?} for {}: {}",
798 P::SERVICE,
799 speaker_id.as_str(),
800 e
801 );
802 }
803 }
804 }
805
806 Ok(self.get_property::<P>(speaker_id))
807 }
808
809 pub fn unwatch_property_with_subscription<P: SonosProperty>(&self, speaker_id: &SpeakerId) {
811 self.unregister_watch(speaker_id, P::KEY);
813
814 if let Some(em) = self.event_manager.get() {
816 if let Some(ip) = self.get_speaker_ip(speaker_id) {
817 if let Err(e) = em.release_service_subscription(ip, P::SERVICE) {
818 tracing::warn!(
819 "Failed to unsubscribe from {:?} for {}: {}",
820 P::SERVICE,
821 speaker_id.as_str(),
822 e
823 );
824 }
825 }
826 }
827 }
828
829 pub fn is_watched(&self, speaker_id: &SpeakerId, property_key: &'static str) -> bool {
831 is_pair_watched(&self.watched.read(), speaker_id, property_key)
832 }
833
834 fn maybe_emit_change(
836 &self,
837 speaker_id: &SpeakerId,
838 property_key: &'static str,
839 service: Service,
840 ) {
841 let is_watched = is_pair_watched(&self.watched.read(), speaker_id, property_key);
842
843 if is_watched {
844 let event = ChangeEvent::new(speaker_id.clone(), property_key, service);
845 let _ = self.event_tx.send(event);
846 }
847 }
848
849 pub fn initialize(&self, topology: Topology) {
851 let mut store = self.store.write();
852 for speaker in &topology.speakers {
853 store.add_speaker(speaker.clone());
854 }
855 for group in &topology.groups {
856 store.add_group(group.clone());
857 }
858 store.set_system(topology);
859 }
860
861 pub fn is_initialized(&self) -> bool {
863 !self.store.read().is_empty()
864 }
865
866 pub fn speaker_count(&self) -> usize {
868 self.store.read().speaker_count()
869 }
870
871 pub fn group_count(&self) -> usize {
873 self.store.read().group_count()
874 }
875
876 pub fn groups(&self) -> Vec<GroupInfo> {
881 self.store.read().groups.values().cloned().collect()
882 }
883
884 pub fn get_group(&self, group_id: &GroupId) -> Option<GroupInfo> {
886 self.store.read().groups.get(group_id).cloned()
887 }
888
889 pub fn get_group_for_speaker(&self, speaker_id: &SpeakerId) -> Option<GroupInfo> {
893 let store = self.store.read();
894 let group_id = store.speaker_to_group.get(speaker_id)?;
895 store.groups.get(group_id).cloned()
896 }
897
898 pub fn resolve_subscription_target(
906 &self,
907 speaker_id: &SpeakerId,
908 speaker_ip: IpAddr,
909 service: Service,
910 ) -> (SpeakerId, IpAddr) {
911 if service.scope() == ServiceScope::PerCoordinator {
912 let store = self.store.read();
913 let coordinator_id = store.resolve_coordinator(speaker_id);
914 if coordinator_id == *speaker_id {
915 (speaker_id.clone(), speaker_ip)
916 } else {
917 let coord_ip = store
918 .speaker(&coordinator_id)
919 .map(|s| s.ip_address)
920 .unwrap_or(speaker_ip);
921 (coordinator_id, coord_ip)
922 }
923 } else {
924 (speaker_id.clone(), speaker_ip)
925 }
926 }
927
928 pub fn event_manager(&self) -> Option<&Arc<SonosEventManager>> {
933 self.event_manager.get()
934 }
935
936 pub fn set_event_manager(&self, em: Arc<SonosEventManager>) -> Result<()> {
941 tracing::debug!("StateManager::set_event_manager called");
942 if self.event_manager.set(Arc::clone(&em)).is_err() {
943 tracing::debug!("Event manager already set — no-op");
944 return Ok(()); }
946
947 em.set_watch_registry(Arc::new(StateWatchRegistry {
949 watched: Arc::clone(&self.watched),
950 ip_to_speaker: Arc::clone(&self.ip_to_speaker),
951 key_to_service: Arc::clone(&self.key_to_service),
952 }));
953
954 let devices_for_em: Vec<_> = self
956 .speaker_infos()
957 .iter()
958 .map(|info| sonos_discovery::Device {
959 id: info.id.as_str().to_string(),
960 name: info.name.clone(),
961 room_name: info.room_name.clone(),
962 ip_address: info.ip_address.to_string(),
963 port: info.port,
964 model_name: info.model_name.clone(),
965 })
966 .collect();
967
968 if let Err(e) = em.add_devices(devices_for_em) {
969 tracing::warn!(
970 "Failed to add devices to event manager during lazy init: {}",
971 e
972 );
973 }
974
975 let worker = spawn_state_event_worker(
977 em,
978 Arc::clone(&self.store),
979 Arc::clone(&self.watched),
980 self.event_tx.clone(),
981 Arc::clone(&self.ip_to_speaker),
982 );
983 info!("StateManager event worker started (lazy init)");
984
985 if let Ok(mut w) = self._worker.lock() {
986 *w = Some(worker);
987 }
988
989 Ok(())
990 }
991
992 pub fn set_event_init(&self, f: EventInitFn) {
997 let _ = self.event_init.set(f);
998 }
999
1000 pub fn event_init(&self) -> Option<&EventInitFn> {
1005 self.event_init.get()
1006 }
1007}
1008
1009impl Clone for StateManager {
1010 fn clone(&self) -> Self {
1011 let event_manager = OnceLock::new();
1012 if let Some(em) = self.event_manager.get() {
1013 let _ = event_manager.set(Arc::clone(em));
1014 }
1015 let event_init = OnceLock::new();
1016 if let Some(f) = self.event_init.get() {
1017 let _ = event_init.set(Arc::clone(f));
1018 }
1019 Self {
1020 store: Arc::clone(&self.store),
1021 watched: Arc::clone(&self.watched),
1022 ip_to_speaker: Arc::clone(&self.ip_to_speaker),
1023 event_manager,
1024 event_tx: self.event_tx.clone(),
1025 event_rx: Arc::clone(&self.event_rx),
1026 _worker: Mutex::new(None),
1027 cleanup_timeout: self.cleanup_timeout,
1028 key_to_service: Arc::clone(&self.key_to_service),
1029 event_init,
1030 }
1031 }
1032}
1033
1034pub struct StateManagerBuilder {
1040 cleanup_timeout: Duration,
1041 event_manager: Option<Arc<SonosEventManager>>,
1042}
1043
1044impl Default for StateManagerBuilder {
1045 fn default() -> Self {
1046 Self {
1047 cleanup_timeout: Duration::from_secs(5),
1048 event_manager: None,
1049 }
1050 }
1051}
1052
1053impl StateManagerBuilder {
1054 pub fn cleanup_timeout(mut self, timeout: Duration) -> Self {
1056 self.cleanup_timeout = timeout;
1057 self
1058 }
1059
1060 pub fn with_event_manager(mut self, em: Arc<SonosEventManager>) -> Self {
1067 self.event_manager = Some(em);
1068 self
1069 }
1070
1071 pub fn build(self) -> Result<StateManager> {
1073 let (event_tx, event_rx) = mpsc::channel();
1074
1075 let store = Arc::new(RwLock::new(StateStore::new()));
1076 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1077 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
1078 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
1079
1080 let event_manager_lock = OnceLock::new();
1081 let mut worker = None;
1082
1083 if let Some(em) = self.event_manager {
1085 let _ = event_manager_lock.set(Arc::clone(&em));
1086
1087 em.set_watch_registry(Arc::new(StateWatchRegistry {
1089 watched: Arc::clone(&watched),
1090 ip_to_speaker: Arc::clone(&ip_to_speaker),
1091 key_to_service: Arc::clone(&key_to_service),
1092 }));
1093
1094 let worker_handle = spawn_state_event_worker(
1095 em,
1096 Arc::clone(&store),
1097 Arc::clone(&watched),
1098 event_tx.clone(),
1099 Arc::clone(&ip_to_speaker),
1100 );
1101 info!("StateManager event worker started");
1102 worker = Some(worker_handle);
1103 }
1104
1105 let manager = StateManager {
1106 store,
1107 watched,
1108 ip_to_speaker,
1109 event_manager: event_manager_lock,
1110 event_tx,
1111 event_rx: Arc::new(Mutex::new(event_rx)),
1112 _worker: Mutex::new(worker),
1113 cleanup_timeout: self.cleanup_timeout,
1114 key_to_service,
1115 event_init: OnceLock::new(),
1116 };
1117
1118 info!("StateManager created (sync-first mode)");
1119 Ok(manager)
1120 }
1121}
1122
1123#[cfg(test)]
1124mod tests {
1125 use super::*;
1126 use crate::property::{GroupVolume, PlaybackState, Volume};
1127 use sonos_api::Service;
1128
1129 #[test]
1130 fn test_state_manager_creation() {
1131 let manager = StateManager::new().unwrap();
1132 assert!(!manager.is_initialized());
1133 assert_eq!(manager.speaker_count(), 0);
1134 }
1135
1136 #[test]
1137 fn test_add_devices() {
1138 let manager = StateManager::new().unwrap();
1139
1140 let devices = vec![Device {
1141 id: "RINCON_123".to_string(),
1142 name: "Living Room".to_string(),
1143 room_name: "Living Room".to_string(),
1144 ip_address: "192.168.1.100".to_string(),
1145 port: 1400,
1146 model_name: "Sonos One".to_string(),
1147 }];
1148
1149 manager.add_devices(devices).unwrap();
1150 assert_eq!(manager.speaker_count(), 1);
1151 }
1152
1153 #[test]
1154 fn test_property_storage() {
1155 let manager = StateManager::new().unwrap();
1156
1157 let devices = vec![Device {
1158 id: "RINCON_123".to_string(),
1159 name: "Living Room".to_string(),
1160 room_name: "Living Room".to_string(),
1161 ip_address: "192.168.1.100".to_string(),
1162 port: 1400,
1163 model_name: "Sonos One".to_string(),
1164 }];
1165 manager.add_devices(devices).unwrap();
1166
1167 let speaker_id = SpeakerId::new("RINCON_123");
1168
1169 assert!(manager.get_property::<Volume>(&speaker_id).is_none());
1171
1172 manager.set_property(&speaker_id, Volume::new(50));
1174 assert_eq!(
1175 manager.get_property::<Volume>(&speaker_id),
1176 Some(Volume::new(50))
1177 );
1178 }
1179
1180 #[test]
1181 fn test_watch_registration() {
1182 let manager = StateManager::new().unwrap();
1183
1184 let devices = vec![Device {
1185 id: "RINCON_123".to_string(),
1186 name: "Living Room".to_string(),
1187 room_name: "Living Room".to_string(),
1188 ip_address: "192.168.1.100".to_string(),
1189 port: 1400,
1190 model_name: "Sonos One".to_string(),
1191 }];
1192 manager.add_devices(devices).unwrap();
1193
1194 let speaker_id = SpeakerId::new("RINCON_123");
1195
1196 assert!(!manager.is_watched(&speaker_id, "volume"));
1198
1199 manager.register_watch(&speaker_id, "volume");
1201 assert!(manager.is_watched(&speaker_id, "volume"));
1202
1203 manager.unregister_watch(&speaker_id, "volume");
1205 assert!(!manager.is_watched(&speaker_id, "volume"));
1206 }
1207
1208 #[test]
1209 fn test_change_event_emission() {
1210 let manager = StateManager::new().unwrap();
1211
1212 let devices = vec![Device {
1213 id: "RINCON_123".to_string(),
1214 name: "Living Room".to_string(),
1215 room_name: "Living Room".to_string(),
1216 ip_address: "192.168.1.100".to_string(),
1217 port: 1400,
1218 model_name: "Sonos One".to_string(),
1219 }];
1220 manager.add_devices(devices).unwrap();
1221
1222 let speaker_id = SpeakerId::new("RINCON_123");
1223
1224 manager.register_watch(&speaker_id, "volume");
1226
1227 manager.set_property(&speaker_id, Volume::new(75));
1229
1230 let iter = manager.iter();
1232 let event = iter.recv_timeout(std::time::Duration::from_millis(100));
1233 assert!(event.is_some());
1234
1235 let event = event.unwrap();
1236 assert_eq!(event.speaker_id.as_str(), "RINCON_123");
1237 assert_eq!(event.property_key, "volume");
1238 }
1239
1240 #[test]
1241 fn test_set_group_property_emits_change_event() {
1242 let manager = StateManager::new().unwrap();
1243
1244 let devices = vec![Device {
1245 id: "RINCON_123".to_string(),
1246 name: "Living Room".to_string(),
1247 room_name: "Living Room".to_string(),
1248 ip_address: "192.168.1.100".to_string(),
1249 port: 1400,
1250 model_name: "Sonos One".to_string(),
1251 }];
1252 manager.add_devices(devices).unwrap();
1253
1254 let speaker_id = SpeakerId::new("RINCON_123");
1255 let group_id = GroupId::new("RINCON_123:1");
1256
1257 {
1259 let mut store = manager.store.write();
1260 store.add_group(GroupInfo::new(
1261 group_id.clone(),
1262 speaker_id.clone(),
1263 vec![speaker_id.clone()],
1264 ));
1265 }
1266
1267 manager.register_watch(&speaker_id, "group_volume");
1269
1270 manager.set_group_property(&group_id, GroupVolume::new(80));
1272
1273 let iter = manager.iter();
1275 let event = iter.recv_timeout(std::time::Duration::from_millis(100));
1276 assert!(event.is_some());
1277
1278 let event = event.unwrap();
1279 assert_eq!(event.speaker_id.as_str(), "RINCON_123");
1280 assert_eq!(event.property_key, "group_volume");
1281 assert_eq!(event.service, Service::GroupRenderingControl);
1282 }
1283
1284 #[test]
1285 fn test_set_group_property_no_event_when_unwatched() {
1286 let manager = StateManager::new().unwrap();
1287
1288 let devices = vec![Device {
1289 id: "RINCON_123".to_string(),
1290 name: "Living Room".to_string(),
1291 room_name: "Living Room".to_string(),
1292 ip_address: "192.168.1.100".to_string(),
1293 port: 1400,
1294 model_name: "Sonos One".to_string(),
1295 }];
1296 manager.add_devices(devices).unwrap();
1297
1298 let speaker_id = SpeakerId::new("RINCON_123");
1299 let group_id = GroupId::new("RINCON_123:1");
1300
1301 {
1302 let mut store = manager.store.write();
1303 store.add_group(GroupInfo::new(
1304 group_id.clone(),
1305 speaker_id.clone(),
1306 vec![speaker_id.clone()],
1307 ));
1308 }
1309
1310 manager.set_group_property(&group_id, GroupVolume::new(50));
1312
1313 let iter = manager.iter();
1314 let event = iter.recv_timeout(std::time::Duration::from_millis(100));
1315 assert!(event.is_none());
1316 }
1317
1318 #[test]
1323 fn test_add_group_updates_speaker_to_group() {
1324 let mut store = StateStore::new();
1325
1326 let speaker1 = SpeakerId::new("RINCON_111");
1327 let speaker2 = SpeakerId::new("RINCON_222");
1328 let group_id = GroupId::new("RINCON_111:1");
1329
1330 let group = GroupInfo::new(
1331 group_id.clone(),
1332 speaker1.clone(),
1333 vec![speaker1.clone(), speaker2.clone()],
1334 );
1335
1336 store.add_group(group);
1337
1338 assert_eq!(store.speaker_to_group.get(&speaker1), Some(&group_id));
1340 assert_eq!(store.speaker_to_group.get(&speaker2), Some(&group_id));
1341 }
1342
1343 #[test]
1344 fn test_add_group_single_speaker() {
1345 let mut store = StateStore::new();
1346
1347 let speaker = SpeakerId::new("RINCON_333");
1348 let group_id = GroupId::new("RINCON_333:1");
1349
1350 let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
1351
1352 store.add_group(group.clone());
1353
1354 assert_eq!(store.speaker_to_group.get(&speaker), Some(&group_id));
1356
1357 assert_eq!(store.groups.get(&group_id), Some(&group));
1359 }
1360
1361 #[test]
1362 fn test_get_group_for_speaker_returns_correct_group() {
1363 let mut store = StateStore::new();
1364
1365 let speaker1 = SpeakerId::new("RINCON_111");
1366 let speaker2 = SpeakerId::new("RINCON_222");
1367 let speaker3 = SpeakerId::new("RINCON_333");
1368 let group1_id = GroupId::new("RINCON_111:1");
1369 let group2_id = GroupId::new("RINCON_333:1");
1370
1371 let group1 = GroupInfo::new(
1373 group1_id.clone(),
1374 speaker1.clone(),
1375 vec![speaker1.clone(), speaker2.clone()],
1376 );
1377
1378 let group2 = GroupInfo::new(group2_id.clone(), speaker3.clone(), vec![speaker3.clone()]);
1380
1381 store.add_group(group1.clone());
1382 store.add_group(group2.clone());
1383
1384 assert_eq!(store.get_group_for_speaker(&speaker1), Some(&group1));
1386 assert_eq!(store.get_group_for_speaker(&speaker2), Some(&group1));
1387 assert_eq!(store.get_group_for_speaker(&speaker3), Some(&group2));
1388 }
1389
1390 #[test]
1391 fn test_get_group_for_speaker_returns_none_for_unknown() {
1392 let store = StateStore::new();
1393
1394 let unknown_speaker = SpeakerId::new("RINCON_UNKNOWN");
1395
1396 assert!(store.get_group_for_speaker(&unknown_speaker).is_none());
1397 }
1398
1399 #[test]
1400 fn test_clear_groups_removes_all_group_data() {
1401 let mut store = StateStore::new();
1402
1403 let speaker1 = SpeakerId::new("RINCON_111");
1404 let speaker2 = SpeakerId::new("RINCON_222");
1405 let group_id = GroupId::new("RINCON_111:1");
1406
1407 let group = GroupInfo::new(
1408 group_id.clone(),
1409 speaker1.clone(),
1410 vec![speaker1.clone(), speaker2.clone()],
1411 );
1412
1413 store.add_group(group);
1414
1415 assert!(!store.groups.is_empty());
1417 assert!(!store.speaker_to_group.is_empty());
1418
1419 store.clear_groups();
1421
1422 assert!(store.groups.is_empty());
1424 assert!(store.group_props.is_empty());
1425 assert!(store.speaker_to_group.is_empty());
1426 }
1427
1428 #[test]
1429 fn test_clear_groups_then_add_new_groups() {
1430 let mut store = StateStore::new();
1431
1432 let speaker1 = SpeakerId::new("RINCON_111");
1434 let group1_id = GroupId::new("RINCON_111:1");
1435 let group1 = GroupInfo::new(group1_id.clone(), speaker1.clone(), vec![speaker1.clone()]);
1436 store.add_group(group1);
1437
1438 store.clear_groups();
1440
1441 let speaker2 = SpeakerId::new("RINCON_222");
1442 let group2_id = GroupId::new("RINCON_222:1");
1443 let group2 = GroupInfo::new(group2_id.clone(), speaker2.clone(), vec![speaker2.clone()]);
1444 store.add_group(group2.clone());
1445
1446 assert!(!store.groups.contains_key(&group1_id));
1448 assert_eq!(store.groups.get(&group2_id), Some(&group2));
1449
1450 assert!(!store.speaker_to_group.contains_key(&speaker1));
1452 assert_eq!(store.speaker_to_group.get(&speaker2), Some(&group2_id));
1453 }
1454
1455 #[test]
1460 fn test_state_manager_groups_returns_all_groups() {
1461 let manager = StateManager::new().unwrap();
1462
1463 let devices = vec![
1465 Device {
1466 id: "RINCON_111".to_string(),
1467 name: "Living Room".to_string(),
1468 room_name: "Living Room".to_string(),
1469 ip_address: "192.168.1.100".to_string(),
1470 port: 1400,
1471 model_name: "Sonos One".to_string(),
1472 },
1473 Device {
1474 id: "RINCON_222".to_string(),
1475 name: "Kitchen".to_string(),
1476 room_name: "Kitchen".to_string(),
1477 ip_address: "192.168.1.101".to_string(),
1478 port: 1400,
1479 model_name: "Sonos One".to_string(),
1480 },
1481 ];
1482 manager.add_devices(devices).unwrap();
1483
1484 let speaker1 = SpeakerId::new("RINCON_111");
1486 let speaker2 = SpeakerId::new("RINCON_222");
1487 let group1 = GroupInfo::new(
1488 GroupId::new("RINCON_111:1"),
1489 speaker1.clone(),
1490 vec![speaker1.clone()],
1491 );
1492 let group2 = GroupInfo::new(
1493 GroupId::new("RINCON_222:1"),
1494 speaker2.clone(),
1495 vec![speaker2.clone()],
1496 );
1497
1498 let topology = Topology::new(
1499 manager.speaker_infos(),
1500 vec![group1.clone(), group2.clone()],
1501 );
1502 manager.initialize(topology);
1503
1504 let groups = manager.groups();
1506 assert_eq!(groups.len(), 2);
1507
1508 let group_ids: Vec<_> = groups.iter().map(|g| g.id.clone()).collect();
1510 assert!(group_ids.contains(&GroupId::new("RINCON_111:1")));
1511 assert!(group_ids.contains(&GroupId::new("RINCON_222:1")));
1512 }
1513
1514 #[test]
1515 fn test_state_manager_groups_returns_empty_when_no_groups() {
1516 let manager = StateManager::new().unwrap();
1517
1518 let groups = manager.groups();
1520 assert!(groups.is_empty());
1521 }
1522
1523 #[test]
1524 fn test_state_manager_get_group_returns_correct_group() {
1525 let manager = StateManager::new().unwrap();
1526
1527 let devices = vec![Device {
1529 id: "RINCON_111".to_string(),
1530 name: "Living Room".to_string(),
1531 room_name: "Living Room".to_string(),
1532 ip_address: "192.168.1.100".to_string(),
1533 port: 1400,
1534 model_name: "Sonos One".to_string(),
1535 }];
1536 manager.add_devices(devices).unwrap();
1537
1538 let speaker = SpeakerId::new("RINCON_111");
1540 let group_id = GroupId::new("RINCON_111:1");
1541 let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
1542
1543 let topology = Topology::new(manager.speaker_infos(), vec![group.clone()]);
1544 manager.initialize(topology);
1545
1546 let found = manager.get_group(&group_id);
1548 assert!(found.is_some());
1549 assert_eq!(found.unwrap(), group);
1550 }
1551
1552 #[test]
1553 fn test_state_manager_get_group_returns_none_for_unknown() {
1554 let manager = StateManager::new().unwrap();
1555
1556 let unknown_id = GroupId::new("RINCON_UNKNOWN:1");
1558 let found = manager.get_group(&unknown_id);
1559 assert!(found.is_none());
1560 }
1561
1562 #[test]
1563 fn test_state_manager_get_group_for_speaker_returns_correct_group() {
1564 let manager = StateManager::new().unwrap();
1565
1566 let devices = vec![
1568 Device {
1569 id: "RINCON_111".to_string(),
1570 name: "Living Room".to_string(),
1571 room_name: "Living Room".to_string(),
1572 ip_address: "192.168.1.100".to_string(),
1573 port: 1400,
1574 model_name: "Sonos One".to_string(),
1575 },
1576 Device {
1577 id: "RINCON_222".to_string(),
1578 name: "Kitchen".to_string(),
1579 room_name: "Kitchen".to_string(),
1580 ip_address: "192.168.1.101".to_string(),
1581 port: 1400,
1582 model_name: "Sonos One".to_string(),
1583 },
1584 ];
1585 manager.add_devices(devices).unwrap();
1586
1587 let speaker1 = SpeakerId::new("RINCON_111");
1589 let speaker2 = SpeakerId::new("RINCON_222");
1590 let group_id = GroupId::new("RINCON_111:1");
1591 let group = GroupInfo::new(
1592 group_id.clone(),
1593 speaker1.clone(),
1594 vec![speaker1.clone(), speaker2.clone()],
1595 );
1596
1597 let topology = Topology::new(manager.speaker_infos(), vec![group.clone()]);
1598 manager.initialize(topology);
1599
1600 let found1 = manager.get_group_for_speaker(&speaker1);
1602 assert!(found1.is_some());
1603 assert_eq!(found1.unwrap(), group);
1604
1605 let found2 = manager.get_group_for_speaker(&speaker2);
1606 assert!(found2.is_some());
1607 assert_eq!(found2.unwrap(), group);
1608 }
1609
1610 #[test]
1611 fn test_state_manager_get_group_for_speaker_returns_none_for_unknown() {
1612 let manager = StateManager::new().unwrap();
1613
1614 let unknown_speaker = SpeakerId::new("RINCON_UNKNOWN");
1616 let found = manager.get_group_for_speaker(&unknown_speaker);
1617 assert!(found.is_none());
1618 }
1619
1620 #[test]
1621 fn test_state_manager_group_methods_consistency() {
1622 let manager = StateManager::new().unwrap();
1623
1624 let devices = vec![Device {
1626 id: "RINCON_111".to_string(),
1627 name: "Living Room".to_string(),
1628 room_name: "Living Room".to_string(),
1629 ip_address: "192.168.1.100".to_string(),
1630 port: 1400,
1631 model_name: "Sonos One".to_string(),
1632 }];
1633 manager.add_devices(devices).unwrap();
1634
1635 let speaker = SpeakerId::new("RINCON_111");
1637 let group_id = GroupId::new("RINCON_111:1");
1638 let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
1639
1640 let topology = Topology::new(manager.speaker_infos(), vec![group.clone()]);
1641 manager.initialize(topology);
1642
1643 let groups = manager.groups();
1645 assert_eq!(groups.len(), 1);
1646 assert_eq!(groups[0], group);
1647
1648 let by_id = manager.get_group(&group_id);
1649 assert_eq!(by_id, Some(group.clone()));
1650
1651 let by_speaker = manager.get_group_for_speaker(&speaker);
1652 assert_eq!(by_speaker, Some(group.clone()));
1653
1654 assert_eq!(groups[0], by_id.unwrap());
1656 assert_eq!(groups[0], by_speaker.unwrap());
1657 }
1658
1659 #[test]
1664 fn test_get_boot_seq_returns_none_for_unknown_speaker() {
1665 let manager = StateManager::new().unwrap();
1666 let unknown = SpeakerId::new("RINCON_UNKNOWN");
1667 assert!(manager.get_boot_seq(&unknown).is_none());
1668 }
1669
1670 #[test]
1671 fn test_boot_seq_defaults_to_zero_for_new_speaker() {
1672 let manager = StateManager::new().unwrap();
1673
1674 let devices = vec![Device {
1675 id: "RINCON_123".to_string(),
1676 name: "Living Room".to_string(),
1677 room_name: "Living Room".to_string(),
1678 ip_address: "192.168.1.100".to_string(),
1679 port: 1400,
1680 model_name: "Sonos One".to_string(),
1681 }];
1682 manager.add_devices(devices).unwrap();
1683
1684 let speaker_id = SpeakerId::new("RINCON_123");
1685
1686 assert_eq!(manager.get_boot_seq(&speaker_id), Some(0));
1688 }
1689
1690 #[test]
1695 fn test_state_watch_registry_register_and_unregister() {
1696 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1697 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
1698 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
1699
1700 let ip: IpAddr = "192.168.1.100".parse().unwrap();
1701 let speaker_id = SpeakerId::new("RINCON_123");
1702 ip_to_speaker.write().insert(ip, speaker_id.clone());
1703
1704 let registry = StateWatchRegistry {
1705 watched: Arc::clone(&watched),
1706 ip_to_speaker: Arc::clone(&ip_to_speaker),
1707 key_to_service: Arc::clone(&key_to_service),
1708 };
1709
1710 registry.register_watch(&speaker_id, "volume", Service::RenderingControl);
1712 registry.register_watch(&speaker_id, "mute", Service::RenderingControl);
1713 registry.register_watch(&speaker_id, "playback_state", Service::AVTransport);
1714
1715 assert_eq!(watched.read().len(), 3);
1716
1717 registry.unregister_watches_for_service(ip, Service::RenderingControl);
1719
1720 let w = watched.read();
1721 assert_eq!(w.len(), 1);
1722 assert!(is_pair_watched(&w, &speaker_id, "playback_state"));
1723 assert!(!is_pair_watched(&w, &speaker_id, "volume"));
1724 assert!(!is_pair_watched(&w, &speaker_id, "mute"));
1725 }
1726
1727 #[test]
1728 fn test_state_watch_registry_unknown_ip_is_noop() {
1729 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1730 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
1731 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
1732
1733 let speaker_id = SpeakerId::new("RINCON_123");
1734
1735 let registry = StateWatchRegistry {
1736 watched: Arc::clone(&watched),
1737 ip_to_speaker,
1738 key_to_service: Arc::clone(&key_to_service),
1739 };
1740
1741 retain_direct_watch(&watched, &speaker_id, "volume");
1743 key_to_service
1744 .write()
1745 .insert("volume", Service::RenderingControl);
1746
1747 let unknown_ip: IpAddr = "10.0.0.1".parse().unwrap();
1749 registry.unregister_watches_for_service(unknown_ip, Service::RenderingControl);
1750
1751 assert_eq!(watched.read().len(), 1);
1753 }
1754
1755 #[test]
1756 fn test_state_watch_registry_only_removes_matching_speaker() {
1757 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1758 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
1759 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
1760
1761 let ip1: IpAddr = "192.168.1.100".parse().unwrap();
1762 let ip2: IpAddr = "192.168.1.101".parse().unwrap();
1763 let speaker1 = SpeakerId::new("RINCON_111");
1764 let speaker2 = SpeakerId::new("RINCON_222");
1765
1766 ip_to_speaker.write().insert(ip1, speaker1.clone());
1767 ip_to_speaker.write().insert(ip2, speaker2.clone());
1768
1769 let registry = StateWatchRegistry {
1770 watched: Arc::clone(&watched),
1771 ip_to_speaker,
1772 key_to_service: Arc::clone(&key_to_service),
1773 };
1774
1775 registry.register_watch(&speaker1, "volume", Service::RenderingControl);
1777 registry.register_watch(&speaker2, "volume", Service::RenderingControl);
1778 assert_eq!(watched.read().len(), 2);
1779
1780 registry.unregister_watches_for_service(ip1, Service::RenderingControl);
1782
1783 let w = watched.read();
1784 assert_eq!(w.len(), 1);
1785 assert!(is_pair_watched(&w, &speaker2, "volume"));
1786 assert!(!is_pair_watched(&w, &speaker1, "volume"));
1787 }
1788
1789 #[test]
1800 fn test_watch_refcount_survives_partial_release() {
1801 let manager = StateManager::new().unwrap();
1802 let speaker_id = SpeakerId::new("RINCON_123");
1803
1804 manager.register_watch(&speaker_id, "volume");
1805 manager.register_watch(&speaker_id, "volume");
1806 assert!(manager.is_watched(&speaker_id, "volume"));
1807
1808 manager.unregister_watch(&speaker_id, "volume");
1810 assert!(
1811 manager.is_watched(&speaker_id, "volume"),
1812 "one of two watchers released — the property must stay watched"
1813 );
1814
1815 manager.unregister_watch(&speaker_id, "volume");
1817 assert!(!manager.is_watched(&speaker_id, "volume"));
1818
1819 manager.unregister_watch(&speaker_id, "volume");
1821 assert!(!manager.is_watched(&speaker_id, "volume"));
1822 }
1823
1824 #[test]
1832 fn test_service_unregister_keeps_directly_held_watches() {
1833 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1834 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
1835 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
1836
1837 let ip: IpAddr = "192.168.1.100".parse().unwrap();
1838 let speaker_id = SpeakerId::new("RINCON_123");
1839 ip_to_speaker.write().insert(ip, speaker_id.clone());
1840
1841 let registry = StateWatchRegistry {
1842 watched: Arc::clone(&watched),
1843 ip_to_speaker,
1844 key_to_service,
1845 };
1846
1847 registry.register_watch(&speaker_id, "volume", Service::RenderingControl);
1849 retain_direct_watch(&watched, &speaker_id, "volume");
1850 registry.register_watch(&speaker_id, "mute", Service::RenderingControl);
1852 retain_direct_watch(&watched, &speaker_id, "mute");
1853
1854 registry.unregister_watches_for_service(ip, Service::RenderingControl);
1855
1856 let w = watched.read();
1857 assert!(
1858 is_pair_watched(&w, &speaker_id, "volume"),
1859 "the direct hold on volume must survive the subscription teardown"
1860 );
1861 assert!(
1862 is_pair_watched(&w, &speaker_id, "mute"),
1863 "the direct hold on mute must survive the subscription teardown"
1864 );
1865 }
1866
1867 #[test]
1879 fn test_set_property_on_group_member_is_readable_from_both() {
1880 let manager = StateManager::new().unwrap();
1881
1882 let devices = vec![
1883 Device {
1884 id: "RINCON_COORD".to_string(),
1885 name: "Living Room".to_string(),
1886 room_name: "Living Room".to_string(),
1887 ip_address: "192.168.1.100".to_string(),
1888 port: 1400,
1889 model_name: "Sonos One".to_string(),
1890 },
1891 Device {
1892 id: "RINCON_MEMBER".to_string(),
1893 name: "Kitchen".to_string(),
1894 room_name: "Kitchen".to_string(),
1895 ip_address: "192.168.1.101".to_string(),
1896 port: 1400,
1897 model_name: "Sonos One".to_string(),
1898 },
1899 ];
1900 manager.add_devices(devices).unwrap();
1901
1902 let coordinator = SpeakerId::new("RINCON_COORD");
1903 let member = SpeakerId::new("RINCON_MEMBER");
1904 let group_id = GroupId::new("RINCON_COORD:1");
1905 let topology = Topology::new(
1906 manager.speaker_infos(),
1907 vec![GroupInfo::new(
1908 group_id,
1909 coordinator.clone(),
1910 vec![coordinator.clone(), member.clone()],
1911 )],
1912 );
1913 manager.initialize(topology);
1914
1915 manager.set_property(&member, PlaybackState::Playing);
1918
1919 assert_eq!(
1920 manager.get_property::<PlaybackState>(&member),
1921 Some(PlaybackState::Playing),
1922 "the member must be able to read back what it just wrote"
1923 );
1924 assert_eq!(
1925 manager.get_property::<PlaybackState>(&coordinator),
1926 Some(PlaybackState::Playing),
1927 "the write belongs in the coordinator's bag, which is where reads resolve"
1928 );
1929
1930 manager.set_property(&member, Volume::new(33));
1932 assert_eq!(
1933 manager.get_property::<Volume>(&member),
1934 Some(Volume::new(33))
1935 );
1936 assert_eq!(
1937 manager.get_property::<Volume>(&coordinator),
1938 None,
1939 "PerSpeaker writes must not be redirected to the coordinator"
1940 );
1941 }
1942
1943 #[test]
1948 fn test_resolve_coordinator_for_standalone_speaker() {
1949 let mut store = StateStore::new();
1950
1951 let speaker = SpeakerId::new("RINCON_111");
1952 let group_id = GroupId::new("RINCON_111:1");
1953
1954 store.add_speaker(SpeakerInfo {
1955 id: speaker.clone(),
1956 name: "Living Room".to_string(),
1957 room_name: "Living Room".to_string(),
1958 ip_address: "192.168.1.100".parse().unwrap(),
1959 port: 1400,
1960 model_name: "Test".to_string(),
1961 software_version: "1.0".to_string(),
1962 boot_seq: 0,
1963 satellites: vec![],
1964 });
1965 store.add_group(GroupInfo::new(
1966 group_id,
1967 speaker.clone(),
1968 vec![speaker.clone()],
1969 ));
1970
1971 assert_eq!(store.resolve_coordinator(&speaker), speaker);
1973 }
1974
1975 #[test]
1976 fn test_resolve_coordinator_for_group_member() {
1977 let mut store = StateStore::new();
1978
1979 let coordinator = SpeakerId::new("RINCON_COORD");
1980 let member = SpeakerId::new("RINCON_MEMBER");
1981 let group_id = GroupId::new("RINCON_COORD:1");
1982
1983 store.add_group(GroupInfo::new(
1984 group_id,
1985 coordinator.clone(),
1986 vec![coordinator.clone(), member.clone()],
1987 ));
1988
1989 assert_eq!(store.resolve_coordinator(&member), coordinator);
1991 assert_eq!(store.resolve_coordinator(&coordinator), coordinator);
1993 }
1994
1995 #[test]
1996 fn test_resolve_coordinator_no_group_data() {
1997 let store = StateStore::new();
1998
1999 let speaker = SpeakerId::new("RINCON_UNKNOWN");
2000
2001 assert_eq!(store.resolve_coordinator(&speaker), speaker);
2003 }
2004
2005 #[test]
2010 fn test_get_resolved_per_coordinator_reads_from_coordinator() {
2011 let mut store = StateStore::new();
2012
2013 let coordinator = SpeakerId::new("RINCON_COORD");
2014 let member = SpeakerId::new("RINCON_MEMBER");
2015 let group_id = GroupId::new("RINCON_COORD:1");
2016
2017 store.add_speaker(SpeakerInfo {
2018 id: coordinator.clone(),
2019 name: "Coord".to_string(),
2020 room_name: "Coord".to_string(),
2021 ip_address: "192.168.1.100".parse().unwrap(),
2022 port: 1400,
2023 model_name: "Test".to_string(),
2024 software_version: "1.0".to_string(),
2025 boot_seq: 0,
2026 satellites: vec![],
2027 });
2028 store.add_speaker(SpeakerInfo {
2029 id: member.clone(),
2030 name: "Member".to_string(),
2031 room_name: "Member".to_string(),
2032 ip_address: "192.168.1.101".parse().unwrap(),
2033 port: 1400,
2034 model_name: "Test".to_string(),
2035 software_version: "1.0".to_string(),
2036 boot_seq: 0,
2037 satellites: vec![],
2038 });
2039 store.add_group(GroupInfo::new(
2040 group_id,
2041 coordinator.clone(),
2042 vec![coordinator.clone(), member.clone()],
2043 ));
2044
2045 store.set(&coordinator, PlaybackState::Playing);
2047
2048 let resolved: Option<PlaybackState> = store.get_resolved(&member);
2050 assert_eq!(resolved, Some(PlaybackState::Playing));
2051
2052 let direct: Option<PlaybackState> = store.get(&member);
2054 assert_eq!(direct, None);
2055 }
2056
2057 #[test]
2058 fn test_get_resolved_per_speaker_reads_own_props() {
2059 let mut store = StateStore::new();
2060
2061 let coordinator = SpeakerId::new("RINCON_COORD");
2062 let member = SpeakerId::new("RINCON_MEMBER");
2063 let group_id = GroupId::new("RINCON_COORD:1");
2064
2065 store.add_speaker(SpeakerInfo {
2066 id: coordinator.clone(),
2067 name: "Coord".to_string(),
2068 room_name: "Coord".to_string(),
2069 ip_address: "192.168.1.100".parse().unwrap(),
2070 port: 1400,
2071 model_name: "Test".to_string(),
2072 software_version: "1.0".to_string(),
2073 boot_seq: 0,
2074 satellites: vec![],
2075 });
2076 store.add_speaker(SpeakerInfo {
2077 id: member.clone(),
2078 name: "Member".to_string(),
2079 room_name: "Member".to_string(),
2080 ip_address: "192.168.1.101".parse().unwrap(),
2081 port: 1400,
2082 model_name: "Test".to_string(),
2083 software_version: "1.0".to_string(),
2084 boot_seq: 0,
2085 satellites: vec![],
2086 });
2087 store.add_group(GroupInfo::new(
2088 group_id,
2089 coordinator.clone(),
2090 vec![coordinator.clone(), member.clone()],
2091 ));
2092
2093 store.set(&coordinator, Volume::new(80));
2095
2096 let resolved: Option<Volume> = store.get_resolved(&member);
2098 assert_eq!(resolved, None);
2099
2100 let coord_resolved: Option<Volume> = store.get_resolved(&coordinator);
2102 assert_eq!(coord_resolved, Some(Volume::new(80)));
2103 }
2104
2105 #[test]
2106 fn test_update_speaker_ip() {
2107 let manager = StateManager::new().unwrap();
2108
2109 let devices = vec![Device {
2110 id: "RINCON_111".to_string(),
2111 name: "Office".to_string(),
2112 room_name: "Office".to_string(),
2113 ip_address: "192.168.4.198".to_string(),
2114 port: 1400,
2115 model_name: "Roam 2".to_string(),
2116 }];
2117 manager.add_devices(devices).unwrap();
2118
2119 let speaker_id = SpeakerId::new("RINCON_111");
2120 let old_ip: IpAddr = "192.168.4.198".parse().unwrap();
2121 let new_ip: IpAddr = "192.168.4.200".parse().unwrap();
2122
2123 assert_eq!(manager.get_speaker_ip(&speaker_id), Some(old_ip));
2125
2126 manager.update_speaker_ip(&speaker_id, new_ip);
2128
2129 assert_eq!(manager.get_speaker_ip(&speaker_id), Some(new_ip));
2131
2132 let ip_map = manager.ip_to_speaker.read();
2134 assert!(!ip_map.contains_key(&old_ip));
2135 assert_eq!(ip_map.get(&new_ip), Some(&speaker_id));
2136 }
2137
2138 #[test]
2139 fn test_update_speaker_ip_no_change() {
2140 let manager = StateManager::new().unwrap();
2141
2142 let devices = vec![Device {
2143 id: "RINCON_111".to_string(),
2144 name: "Office".to_string(),
2145 room_name: "Office".to_string(),
2146 ip_address: "192.168.4.198".to_string(),
2147 port: 1400,
2148 model_name: "Roam 2".to_string(),
2149 }];
2150 manager.add_devices(devices).unwrap();
2151
2152 let speaker_id = SpeakerId::new("RINCON_111");
2153 let same_ip: IpAddr = "192.168.4.198".parse().unwrap();
2154
2155 manager.update_speaker_ip(&speaker_id, same_ip);
2157 assert_eq!(manager.get_speaker_ip(&speaker_id), Some(same_ip));
2158 }
2159
2160 #[test]
2161 fn test_satellite_ids() {
2162 let manager = StateManager::new().unwrap();
2163
2164 assert!(manager.get_satellite_ids().is_empty());
2165
2166 let ids = vec![SpeakerId::new("RINCON_SAT1"), SpeakerId::new("RINCON_SAT2")];
2167 manager.set_satellite_ids(ids.clone());
2168
2169 let stored = manager.get_satellite_ids();
2170 assert_eq!(stored.len(), 2);
2171 assert!(stored.contains(&SpeakerId::new("RINCON_SAT1")));
2172 assert!(stored.contains(&SpeakerId::new("RINCON_SAT2")));
2173 }
2174}