1use std::any::{Any, TypeId};
29use std::collections::{HashMap, HashSet};
30use std::net::IpAddr;
31use std::sync::{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::decoder::PropertyChange;
43use crate::event_worker::spawn_state_event_worker;
44use crate::iter::{ChangeIterator, EventFanout};
45use crate::model::{GroupId, SpeakerId, SpeakerInfo};
46use crate::property::{GroupInfo, Property, Scope, SonosProperty, Topology};
47use crate::{Result, StateError};
48
49pub type EventInitFn = Arc<
55 dyn Fn() -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> + Send + Sync,
56>;
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum ChangeSource {
71 Event,
76 LocalAction,
81 Fetch,
86}
87
88impl ChangeSource {
89 fn rank(self) -> u8 {
94 match self {
95 ChangeSource::Event => 2,
96 ChangeSource::LocalAction => 1,
97 ChangeSource::Fetch => 0,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy)]
116pub struct WriteStamp {
117 pub observed_at: Instant,
119 pub source: ChangeSource,
121}
122
123impl WriteStamp {
124 pub fn now(source: ChangeSource) -> Self {
129 Self {
130 observed_at: Instant::now(),
131 source,
132 }
133 }
134
135 pub fn observed_at(source: ChangeSource, observed_at: Instant) -> Self {
140 Self {
141 observed_at,
142 source,
143 }
144 }
145
146 fn supersedes(&self, prev: &WriteStamp) -> bool {
152 self.observed_at > prev.observed_at
153 || (self.observed_at == prev.observed_at && self.source.rank() >= prev.source.rank())
154 }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum WriteOutcome {
164 Changed,
166 Unchanged,
168 Stale,
170}
171
172impl WriteOutcome {
173 pub fn changed(self) -> bool {
175 matches!(self, WriteOutcome::Changed)
176 }
177}
178
179#[derive(Debug, Clone)]
195pub struct ChangeEvent {
196 pub speaker_id: SpeakerId,
198 pub change: PropertyChange,
200 pub source: ChangeSource,
202 pub timestamp: Instant,
204}
205
206impl ChangeEvent {
207 pub fn new(speaker_id: SpeakerId, change: PropertyChange, stamp: WriteStamp) -> Self {
208 Self {
209 speaker_id,
210 change,
211 source: stamp.source,
212 timestamp: stamp.observed_at,
213 }
214 }
215
216 pub fn property_key(&self) -> &'static str {
218 self.change.key()
219 }
220
221 pub fn service(&self) -> Service {
223 self.change.service()
224 }
225}
226
227#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
259pub(crate) struct WatchHolds {
260 subscription: bool,
262 direct: usize,
264}
265
266impl WatchHolds {
267 fn is_held(&self) -> bool {
268 self.subscription || self.direct > 0
269 }
270}
271
272pub(crate) type WatchCounts = HashMap<(SpeakerId, &'static str), WatchHolds>;
274
275fn retain_subscription_watch(
277 watched: &RwLock<WatchCounts>,
278 speaker_id: &SpeakerId,
279 key: &'static str,
280) {
281 watched
282 .write()
283 .entry((speaker_id.clone(), key))
284 .or_default()
285 .subscription = true;
286}
287
288pub(crate) fn retain_direct_watch(
290 watched: &RwLock<WatchCounts>,
291 speaker_id: &SpeakerId,
292 key: &'static str,
293) {
294 watched
295 .write()
296 .entry((speaker_id.clone(), key))
297 .or_default()
298 .direct += 1;
299}
300
301fn release_direct_watch(watched: &RwLock<WatchCounts>, speaker_id: &SpeakerId, key: &'static str) {
306 let mut guard = watched.write();
307 let entry_key = (speaker_id.clone(), key);
308 if let Some(holds) = guard.get_mut(&entry_key) {
309 holds.direct = holds.direct.saturating_sub(1);
310 if !holds.is_held() {
311 guard.remove(&entry_key);
312 }
313 }
314}
315
316fn release_subscription_watch(
322 watched: &RwLock<WatchCounts>,
323 speaker_id: &SpeakerId,
324 key: &'static str,
325) {
326 let mut guard = watched.write();
327 let entry_key = (speaker_id.clone(), key);
328 if let Some(holds) = guard.get_mut(&entry_key) {
329 holds.subscription = false;
330 if !holds.is_held() {
331 guard.remove(&entry_key);
332 }
333 }
334}
335
336pub(crate) fn is_pair_watched(
338 watched: &WatchCounts,
339 speaker_id: &SpeakerId,
340 key: &'static str,
341) -> bool {
342 watched.contains_key(&(speaker_id.clone(), key))
343}
344
345pub struct StateStore {
351 pub(crate) speakers: HashMap<SpeakerId, SpeakerInfo>,
353 pub(crate) ip_to_speaker: HashMap<IpAddr, SpeakerId>,
355 pub(crate) speaker_props: HashMap<SpeakerId, PropertyBag>,
357 pub(crate) groups: HashMap<GroupId, GroupInfo>,
359 pub(crate) group_props: HashMap<GroupId, PropertyBag>,
361 pub(crate) system_props: PropertyBag,
363 pub(crate) speaker_to_group: HashMap<SpeakerId, GroupId>,
365 pub(crate) satellite_ids: HashSet<SpeakerId>,
367}
368
369impl StateStore {
370 pub(crate) fn new() -> Self {
371 Self {
372 speakers: HashMap::new(),
373 ip_to_speaker: HashMap::new(),
374 speaker_props: HashMap::new(),
375 groups: HashMap::new(),
376 group_props: HashMap::new(),
377 system_props: PropertyBag::new(),
378 speaker_to_group: HashMap::new(),
379 satellite_ids: HashSet::new(),
380 }
381 }
382
383 pub(crate) fn add_speaker(&mut self, speaker: SpeakerInfo) {
384 let id = speaker.id.clone();
385 let ip = speaker.ip_address;
386 self.ip_to_speaker.insert(ip, id.clone());
387 self.speakers.insert(id.clone(), speaker);
388 self.speaker_props
389 .entry(id)
390 .or_insert_with(PropertyBag::new);
391 }
392
393 fn speaker(&self, id: &SpeakerId) -> Option<&SpeakerInfo> {
394 self.speakers.get(id)
395 }
396
397 fn speakers(&self) -> Vec<SpeakerInfo> {
398 self.speakers.values().cloned().collect()
399 }
400
401 pub(crate) fn add_group(&mut self, group: GroupInfo) {
402 let id = group.id.clone();
403 for member_id in &group.member_ids {
405 self.speaker_to_group.insert(member_id.clone(), id.clone());
406 }
407 self.groups.insert(id.clone(), group);
408 self.group_props.entry(id).or_insert_with(PropertyBag::new);
409 }
410
411 #[allow(dead_code)]
413 pub(crate) fn get_group_for_speaker(&self, speaker_id: &SpeakerId) -> Option<&GroupInfo> {
414 let group_id = self.speaker_to_group.get(speaker_id)?;
415 self.groups.get(group_id)
416 }
417
418 pub(crate) fn clear_groups(&mut self) {
422 self.groups.clear();
423 self.group_props.clear();
424 self.speaker_to_group.clear();
425 }
426
427 pub(crate) fn resolve_coordinator(&self, speaker_id: &SpeakerId) -> SpeakerId {
432 self.speaker_to_group
433 .get(speaker_id)
434 .and_then(|gid| self.groups.get(gid))
435 .map(|group| group.coordinator_id.clone())
436 .unwrap_or_else(|| speaker_id.clone())
437 }
438
439 pub(crate) fn get_resolved<P: SonosProperty>(&self, speaker_id: &SpeakerId) -> Option<P> {
449 if P::SERVICE.scope() == ServiceScope::PerCoordinator && P::SCOPE == Scope::Speaker {
450 let coordinator_id = self.resolve_coordinator(speaker_id);
451 self.speaker_props.get(&coordinator_id)?.get::<P>()
452 } else {
453 self.speaker_props.get(speaker_id)?.get::<P>()
454 }
455 }
456
457 pub(crate) fn resolve_write_target<P: SonosProperty>(
463 &self,
464 speaker_id: &SpeakerId,
465 ) -> SpeakerId {
466 if P::SERVICE.scope() == ServiceScope::PerCoordinator && P::SCOPE == Scope::Speaker {
467 self.resolve_coordinator(speaker_id)
468 } else {
469 speaker_id.clone()
470 }
471 }
472
473 #[cfg_attr(not(test), allow(dead_code))]
474 pub(crate) fn get<P: Property>(&self, speaker_id: &SpeakerId) -> Option<P> {
475 self.speaker_props.get(speaker_id)?.get::<P>()
476 }
477
478 pub(crate) fn set<P: Property>(
479 &mut self,
480 speaker_id: &SpeakerId,
481 value: P,
482 stamp: WriteStamp,
483 ) -> WriteOutcome {
484 let bag = self
485 .speaker_props
486 .entry(speaker_id.clone())
487 .or_insert_with(PropertyBag::new);
488 bag.set(value, stamp)
489 }
490
491 pub(crate) fn get_group<P: Property>(&self, group_id: &GroupId) -> Option<P> {
492 self.group_props.get(group_id)?.get::<P>()
493 }
494
495 pub(crate) fn set_group<P: Property>(
496 &mut self,
497 group_id: &GroupId,
498 value: P,
499 stamp: WriteStamp,
500 ) -> WriteOutcome {
501 let bag = self
502 .group_props
503 .entry(group_id.clone())
504 .or_insert_with(PropertyBag::new);
505 bag.set(value, stamp)
506 }
507
508 fn set_system<P: Property>(&mut self, value: P, stamp: WriteStamp) -> WriteOutcome {
509 self.system_props.set(value, stamp)
510 }
511
512 pub(crate) fn update_speaker_ip_address(
514 &mut self,
515 speaker_id: &SpeakerId,
516 new_ip: IpAddr,
517 ) -> Option<IpAddr> {
518 if let Some(info) = self.speakers.get_mut(speaker_id) {
519 let old_ip = info.ip_address;
520 if old_ip != new_ip {
521 info.ip_address = new_ip;
522 return Some(old_ip);
523 }
524 }
525 None
526 }
527
528 fn is_empty(&self) -> bool {
529 self.speakers.is_empty()
530 }
531
532 fn speaker_count(&self) -> usize {
533 self.speakers.len()
534 }
535
536 fn group_count(&self) -> usize {
537 self.groups.len()
538 }
539}
540
541pub(crate) struct PropertyBag {
546 values: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
548 stamps: HashMap<TypeId, WriteStamp>,
554}
555
556impl PropertyBag {
557 pub(crate) fn new() -> Self {
558 Self {
559 values: HashMap::new(),
560 stamps: HashMap::new(),
561 }
562 }
563
564 fn get<P: Property>(&self) -> Option<P> {
565 let type_id = TypeId::of::<P>();
566 self.values
567 .get(&type_id)
568 .and_then(|boxed| boxed.downcast_ref::<P>())
569 .cloned()
570 }
571
572 fn set<P: Property>(&mut self, value: P, stamp: WriteStamp) -> WriteOutcome {
579 let type_id = TypeId::of::<P>();
580
581 if let Some(prev) = self.stamps.get(&type_id) {
582 if !stamp.supersedes(prev) {
583 tracing::debug!(
584 "Rejecting stale {:?} write for {}: observed {:?} before the stored {:?} write",
585 stamp.source,
586 P::KEY,
587 stamp.observed_at,
588 prev.source,
589 );
590 return WriteOutcome::Stale;
591 }
592 }
593
594 let current = self
595 .values
596 .get(&type_id)
597 .and_then(|boxed| boxed.downcast_ref::<P>());
598 let changed = current != Some(&value);
599
600 self.stamps.insert(type_id, stamp);
604
605 if changed {
606 self.values.insert(type_id, Box::new(value));
607 WriteOutcome::Changed
608 } else {
609 WriteOutcome::Unchanged
610 }
611 }
612}
613
614pub struct StateManager {
623 store: Arc<RwLock<StateStore>>,
625
626 watched: Arc<RwLock<WatchCounts>>,
636
637 ip_to_speaker: Arc<RwLock<HashMap<IpAddr, SpeakerId>>>,
639
640 event_manager: OnceLock<Arc<SonosEventManager>>,
642
643 fanout: Arc<EventFanout>,
650
651 _worker: Mutex<Option<JoinHandle<()>>>,
653
654 cleanup_timeout: Duration,
656
657 key_to_service: Arc<RwLock<HashMap<&'static str, Service>>>,
660
661 event_init: OnceLock<EventInitFn>,
664}
665
666struct StateWatchRegistry {
676 watched: Arc<RwLock<WatchCounts>>,
677 ip_to_speaker: Arc<RwLock<HashMap<IpAddr, SpeakerId>>>,
678 key_to_service: Arc<RwLock<HashMap<&'static str, Service>>>,
679}
680
681impl WatchRegistry for StateWatchRegistry {
682 fn register_watch(&self, speaker_id: &SpeakerId, key: &'static str, service: Service) {
683 retain_subscription_watch(&self.watched, speaker_id, key);
684 self.key_to_service.write().insert(key, service);
685 }
686
687 fn unregister_watches_for_service(&self, ip: IpAddr, service: Service) {
688 let speaker_id = match self.ip_to_speaker.read().get(&ip).cloned() {
690 Some(id) => id,
691 None => {
692 tracing::warn!(
693 "unregister_watches_for_service: no speaker found for IP {}",
694 ip
695 );
696 return;
697 }
698 };
699
700 let service_keys: Vec<&'static str> = self
702 .key_to_service
703 .read()
704 .iter()
705 .filter(|(_, &svc)| svc == service)
706 .map(|(&key, _)| key)
707 .collect();
708
709 for key in service_keys {
716 release_subscription_watch(&self.watched, &speaker_id, key);
717 }
718 }
719}
720
721impl StateManager {
722 pub fn new() -> Result<Self> {
730 Self::builder().build()
731 }
732
733 pub fn builder() -> StateManagerBuilder {
735 StateManagerBuilder::default()
736 }
737
738 pub fn add_devices(&self, devices: Vec<Device>) -> Result<()> {
747 let mut store = self.store.write();
748 let mut ip_map = self.ip_to_speaker.write();
749
750 for device in devices {
751 let speaker_id = SpeakerId::new(&device.id);
752 let ip: IpAddr = device
753 .ip_address
754 .parse()
755 .map_err(|_| StateError::InvalidIpAddress(device.ip_address.clone()))?;
756
757 let friendly_name = if device.room_name.is_empty() || device.room_name == "Unknown" {
758 device.name.clone()
759 } else {
760 device.room_name.clone()
761 };
762
763 let info = SpeakerInfo {
764 id: speaker_id.clone(),
765 name: friendly_name,
766 room_name: device.room_name.clone(),
767 ip_address: ip,
768 port: device.port,
769 model_name: device.model_name.clone(),
770 software_version: "unknown".to_string(),
771 boot_seq: 0,
772 satellites: vec![],
773 };
774
775 ip_map.insert(ip, speaker_id.clone());
777 tracing::debug!(
778 "Added speaker {} at IP {} to ip_to_speaker map",
779 speaker_id.as_str(),
780 ip
781 );
782
783 store.add_speaker(info);
784 }
785
786 drop(store);
788 drop(ip_map);
789
790 if let Some(em) = self.event_manager.get() {
791 let devices_for_em: Vec<_> = self
792 .speaker_infos()
793 .iter()
794 .map(|info| sonos_discovery::Device {
795 id: info.id.as_str().to_string(),
796 name: info.name.clone(),
797 room_name: info.room_name.clone(),
798 ip_address: info.ip_address.to_string(),
799 port: info.port,
800 model_name: info.model_name.clone(),
801 })
802 .collect();
803
804 if let Err(e) = em.add_devices(devices_for_em) {
805 tracing::warn!("Failed to add devices to event manager: {}", e);
806 }
807 }
808
809 Ok(())
810 }
811
812 pub fn speaker_infos(&self) -> Vec<SpeakerInfo> {
814 self.store.read().speakers()
815 }
816
817 pub fn speaker_info(&self, speaker_id: &SpeakerId) -> Option<SpeakerInfo> {
819 self.store.read().speaker(speaker_id).cloned()
820 }
821
822 pub fn get_speaker_ip(&self, speaker_id: &SpeakerId) -> Option<IpAddr> {
824 self.store.read().speaker(speaker_id).map(|s| s.ip_address)
825 }
826
827 pub fn get_boot_seq(&self, speaker_id: &SpeakerId) -> Option<u32> {
829 self.store.read().speaker(speaker_id).map(|s| s.boot_seq)
830 }
831
832 pub fn update_speaker_ip(&self, speaker_id: &SpeakerId, new_ip: IpAddr) {
834 let old_ip = {
835 let mut store = self.store.write();
836 store.update_speaker_ip_address(speaker_id, new_ip)
837 };
838 if let Some(old_ip) = old_ip {
839 let mut map = self.ip_to_speaker.write();
840 map.remove(&old_ip);
841 map.insert(new_ip, speaker_id.clone());
842 }
843 }
844
845 pub fn get_satellite_ids(&self) -> Vec<SpeakerId> {
847 self.store.read().satellite_ids.iter().cloned().collect()
848 }
849
850 pub fn set_satellite_ids(&self, ids: Vec<SpeakerId>) {
852 self.store.write().satellite_ids = ids.into_iter().collect();
853 }
854
855 pub fn iter(&self) -> ChangeIterator {
882 ChangeIterator::new(&self.fanout)
883 }
884
885 pub fn get_property<P: SonosProperty>(&self, speaker_id: &SpeakerId) -> Option<P> {
890 self.store.read().get_resolved::<P>(speaker_id)
891 }
892
893 pub fn get_group_property<P: Property>(&self, group_id: &GroupId) -> Option<P> {
895 self.store.read().get_group::<P>(group_id)
896 }
897
898 pub fn set_property<P: SonosProperty>(&self, speaker_id: &SpeakerId, value: P) {
919 self.set_property_stamped(
920 speaker_id,
921 value,
922 WriteStamp::now(ChangeSource::LocalAction),
923 );
924 }
925
926 pub fn set_property_stamped<P: SonosProperty>(
932 &self,
933 speaker_id: &SpeakerId,
934 value: P,
935 stamp: WriteStamp,
936 ) -> WriteOutcome {
937 let (target_id, outcome) = {
941 let mut store = self.store.write();
942 let target_id = store.resolve_write_target::<P>(speaker_id);
943 let outcome = store.set::<P>(&target_id, value.clone(), stamp);
944 (target_id, outcome)
945 };
946
947 if outcome.changed() {
948 self.maybe_emit_change(speaker_id, &value, stamp);
951 if target_id != *speaker_id {
954 self.maybe_emit_change(&target_id, &value, stamp);
955 }
956 }
957
958 outcome
959 }
960
961 pub fn set_group_property<P: SonosProperty>(&self, group_id: &GroupId, value: P) {
970 self.set_group_property_stamped(
971 group_id,
972 value,
973 WriteStamp::now(ChangeSource::LocalAction),
974 );
975 }
976
977 pub fn set_group_property_stamped<P: SonosProperty>(
982 &self,
983 group_id: &GroupId,
984 value: P,
985 stamp: WriteStamp,
986 ) -> WriteOutcome {
987 let (outcome, coordinator_id) = {
988 let mut store = self.store.write();
989 let outcome = store.set_group::<P>(group_id, value.clone(), stamp);
990 if !outcome.changed() {
991 return outcome;
992 }
993 let coordinator_id = store.groups.get(group_id).map(|g| g.coordinator_id.clone());
994 (outcome, coordinator_id)
995 };
996
997 if let Some(coordinator_id) = coordinator_id {
998 self.maybe_emit_change(&coordinator_id, &value, stamp);
999 }
1000
1001 outcome
1002 }
1003
1004 pub fn register_watch(&self, speaker_id: &SpeakerId, property_key: &'static str) {
1009 retain_direct_watch(&self.watched, speaker_id, property_key);
1010 }
1011
1012 pub fn unregister_watch(&self, speaker_id: &SpeakerId, property_key: &'static str) {
1019 release_direct_watch(&self.watched, speaker_id, property_key);
1020 }
1021
1022 pub fn watch_property_with_subscription<P: SonosProperty>(
1030 &self,
1031 speaker_id: &SpeakerId,
1032 ) -> Result<Option<P>> {
1033 self.register_watch(speaker_id, P::KEY);
1035
1036 if let Some(em) = self.event_manager.get() {
1038 if let Some(ip) = self.get_speaker_ip(speaker_id) {
1040 if let Err(e) = em.ensure_service_subscribed(ip, P::SERVICE) {
1041 tracing::warn!(
1042 "Failed to subscribe to {:?} for {}: {}",
1043 P::SERVICE,
1044 speaker_id.as_str(),
1045 e
1046 );
1047 }
1048 }
1049 }
1050
1051 Ok(self.get_property::<P>(speaker_id))
1052 }
1053
1054 pub fn unwatch_property_with_subscription<P: SonosProperty>(&self, speaker_id: &SpeakerId) {
1056 self.unregister_watch(speaker_id, P::KEY);
1058
1059 if let Some(em) = self.event_manager.get() {
1061 if let Some(ip) = self.get_speaker_ip(speaker_id) {
1062 if let Err(e) = em.release_service_subscription(ip, P::SERVICE) {
1063 tracing::warn!(
1064 "Failed to unsubscribe from {:?} for {}: {}",
1065 P::SERVICE,
1066 speaker_id.as_str(),
1067 e
1068 );
1069 }
1070 }
1071 }
1072 }
1073
1074 pub fn is_watched(&self, speaker_id: &SpeakerId, property_key: &'static str) -> bool {
1076 is_pair_watched(&self.watched.read(), speaker_id, property_key)
1077 }
1078
1079 fn maybe_emit_change<P: SonosProperty>(
1081 &self,
1082 speaker_id: &SpeakerId,
1083 value: &P,
1084 stamp: WriteStamp,
1085 ) {
1086 let is_watched = is_pair_watched(&self.watched.read(), speaker_id, P::KEY);
1087 if !is_watched {
1088 return;
1089 }
1090
1091 let Some(change) = value.to_change() else {
1097 tracing::warn!(
1098 "Not emitting a change event for {}: no PropertyChange variant \
1099 (SonosProperty::to_change returned None)",
1100 P::KEY
1101 );
1102 return;
1103 };
1104
1105 self.fanout
1106 .send(ChangeEvent::new(speaker_id.clone(), change, stamp));
1107 }
1108
1109 pub fn initialize(&self, topology: Topology) {
1111 let mut store = self.store.write();
1112 for speaker in &topology.speakers {
1113 store.add_speaker(speaker.clone());
1114 }
1115 for group in &topology.groups {
1116 store.add_group(group.clone());
1117 }
1118 store.set_system(topology, WriteStamp::now(ChangeSource::LocalAction));
1122 }
1123
1124 pub fn is_initialized(&self) -> bool {
1126 !self.store.read().is_empty()
1127 }
1128
1129 pub fn speaker_count(&self) -> usize {
1131 self.store.read().speaker_count()
1132 }
1133
1134 pub fn group_count(&self) -> usize {
1136 self.store.read().group_count()
1137 }
1138
1139 pub fn groups(&self) -> Vec<GroupInfo> {
1144 self.store.read().groups.values().cloned().collect()
1145 }
1146
1147 pub fn get_group(&self, group_id: &GroupId) -> Option<GroupInfo> {
1149 self.store.read().groups.get(group_id).cloned()
1150 }
1151
1152 pub fn get_group_for_speaker(&self, speaker_id: &SpeakerId) -> Option<GroupInfo> {
1156 let store = self.store.read();
1157 let group_id = store.speaker_to_group.get(speaker_id)?;
1158 store.groups.get(group_id).cloned()
1159 }
1160
1161 pub fn resolve_subscription_target(
1169 &self,
1170 speaker_id: &SpeakerId,
1171 speaker_ip: IpAddr,
1172 service: Service,
1173 ) -> (SpeakerId, IpAddr) {
1174 if service.scope() == ServiceScope::PerCoordinator {
1175 let store = self.store.read();
1176 let coordinator_id = store.resolve_coordinator(speaker_id);
1177 if coordinator_id == *speaker_id {
1178 (speaker_id.clone(), speaker_ip)
1179 } else {
1180 let coord_ip = store
1181 .speaker(&coordinator_id)
1182 .map(|s| s.ip_address)
1183 .unwrap_or(speaker_ip);
1184 (coordinator_id, coord_ip)
1185 }
1186 } else {
1187 (speaker_id.clone(), speaker_ip)
1188 }
1189 }
1190
1191 pub fn event_manager(&self) -> Option<&Arc<SonosEventManager>> {
1196 self.event_manager.get()
1197 }
1198
1199 pub fn set_event_manager(&self, em: Arc<SonosEventManager>) -> Result<()> {
1204 tracing::debug!("StateManager::set_event_manager called");
1205 if self.event_manager.set(Arc::clone(&em)).is_err() {
1206 tracing::debug!("Event manager already set — no-op");
1207 return Ok(()); }
1209
1210 em.set_watch_registry(Arc::new(StateWatchRegistry {
1212 watched: Arc::clone(&self.watched),
1213 ip_to_speaker: Arc::clone(&self.ip_to_speaker),
1214 key_to_service: Arc::clone(&self.key_to_service),
1215 }));
1216
1217 let devices_for_em: Vec<_> = self
1219 .speaker_infos()
1220 .iter()
1221 .map(|info| sonos_discovery::Device {
1222 id: info.id.as_str().to_string(),
1223 name: info.name.clone(),
1224 room_name: info.room_name.clone(),
1225 ip_address: info.ip_address.to_string(),
1226 port: info.port,
1227 model_name: info.model_name.clone(),
1228 })
1229 .collect();
1230
1231 if let Err(e) = em.add_devices(devices_for_em) {
1232 tracing::warn!(
1233 "Failed to add devices to event manager during lazy init: {}",
1234 e
1235 );
1236 }
1237
1238 let worker = spawn_state_event_worker(
1240 em,
1241 Arc::clone(&self.store),
1242 Arc::clone(&self.watched),
1243 Arc::clone(&self.fanout),
1244 Arc::clone(&self.ip_to_speaker),
1245 );
1246 info!("StateManager event worker started (lazy init)");
1247
1248 if let Ok(mut w) = self._worker.lock() {
1249 *w = Some(worker);
1250 }
1251
1252 Ok(())
1253 }
1254
1255 pub fn set_event_init(&self, f: EventInitFn) {
1260 let _ = self.event_init.set(f);
1261 }
1262
1263 pub fn event_init(&self) -> Option<&EventInitFn> {
1268 self.event_init.get()
1269 }
1270}
1271
1272impl Clone for StateManager {
1273 fn clone(&self) -> Self {
1274 let event_manager = OnceLock::new();
1275 if let Some(em) = self.event_manager.get() {
1276 let _ = event_manager.set(Arc::clone(em));
1277 }
1278 let event_init = OnceLock::new();
1279 if let Some(f) = self.event_init.get() {
1280 let _ = event_init.set(Arc::clone(f));
1281 }
1282 Self {
1283 store: Arc::clone(&self.store),
1284 watched: Arc::clone(&self.watched),
1285 ip_to_speaker: Arc::clone(&self.ip_to_speaker),
1286 event_manager,
1287 fanout: Arc::clone(&self.fanout),
1288 _worker: Mutex::new(None),
1289 cleanup_timeout: self.cleanup_timeout,
1290 key_to_service: Arc::clone(&self.key_to_service),
1291 event_init,
1292 }
1293 }
1294}
1295
1296pub struct StateManagerBuilder {
1302 cleanup_timeout: Duration,
1303 event_manager: Option<Arc<SonosEventManager>>,
1304}
1305
1306impl Default for StateManagerBuilder {
1307 fn default() -> Self {
1308 Self {
1309 cleanup_timeout: Duration::from_secs(5),
1310 event_manager: None,
1311 }
1312 }
1313}
1314
1315impl StateManagerBuilder {
1316 pub fn cleanup_timeout(mut self, timeout: Duration) -> Self {
1318 self.cleanup_timeout = timeout;
1319 self
1320 }
1321
1322 pub fn with_event_manager(mut self, em: Arc<SonosEventManager>) -> Self {
1329 self.event_manager = Some(em);
1330 self
1331 }
1332
1333 pub fn build(self) -> Result<StateManager> {
1335 let fanout = Arc::new(EventFanout::new());
1336
1337 let store = Arc::new(RwLock::new(StateStore::new()));
1338 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1339 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
1340 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
1341
1342 let event_manager_lock = OnceLock::new();
1343 let mut worker = None;
1344
1345 if let Some(em) = self.event_manager {
1347 let _ = event_manager_lock.set(Arc::clone(&em));
1348
1349 em.set_watch_registry(Arc::new(StateWatchRegistry {
1351 watched: Arc::clone(&watched),
1352 ip_to_speaker: Arc::clone(&ip_to_speaker),
1353 key_to_service: Arc::clone(&key_to_service),
1354 }));
1355
1356 let worker_handle = spawn_state_event_worker(
1357 em,
1358 Arc::clone(&store),
1359 Arc::clone(&watched),
1360 Arc::clone(&fanout),
1361 Arc::clone(&ip_to_speaker),
1362 );
1363 info!("StateManager event worker started");
1364 worker = Some(worker_handle);
1365 }
1366
1367 let manager = StateManager {
1368 store,
1369 watched,
1370 ip_to_speaker,
1371 event_manager: event_manager_lock,
1372 fanout,
1373 _worker: Mutex::new(worker),
1374 cleanup_timeout: self.cleanup_timeout,
1375 key_to_service,
1376 event_init: OnceLock::new(),
1377 };
1378
1379 info!("StateManager created (sync-first mode)");
1380 Ok(manager)
1381 }
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386 use super::*;
1387 use crate::iter::ChangeIterator;
1388 use crate::property::{GroupVolume, PlaybackState, Volume};
1389 use sonos_api::Service;
1390
1391 fn test_stamp() -> WriteStamp {
1393 WriteStamp::now(ChangeSource::Event)
1394 }
1395
1396 #[test]
1397 fn test_state_manager_creation() {
1398 let manager = StateManager::new().unwrap();
1399 assert!(!manager.is_initialized());
1400 assert_eq!(manager.speaker_count(), 0);
1401 }
1402
1403 #[test]
1404 fn test_add_devices() {
1405 let manager = StateManager::new().unwrap();
1406
1407 let devices = vec![Device {
1408 id: "RINCON_123".to_string(),
1409 name: "Living Room".to_string(),
1410 room_name: "Living Room".to_string(),
1411 ip_address: "192.168.1.100".to_string(),
1412 port: 1400,
1413 model_name: "Sonos One".to_string(),
1414 }];
1415
1416 manager.add_devices(devices).unwrap();
1417 assert_eq!(manager.speaker_count(), 1);
1418 }
1419
1420 #[test]
1421 fn test_property_storage() {
1422 let manager = StateManager::new().unwrap();
1423
1424 let devices = vec![Device {
1425 id: "RINCON_123".to_string(),
1426 name: "Living Room".to_string(),
1427 room_name: "Living Room".to_string(),
1428 ip_address: "192.168.1.100".to_string(),
1429 port: 1400,
1430 model_name: "Sonos One".to_string(),
1431 }];
1432 manager.add_devices(devices).unwrap();
1433
1434 let speaker_id = SpeakerId::new("RINCON_123");
1435
1436 assert!(manager.get_property::<Volume>(&speaker_id).is_none());
1438
1439 manager.set_property(&speaker_id, Volume::new(50));
1441 assert_eq!(
1442 manager.get_property::<Volume>(&speaker_id),
1443 Some(Volume::new(50))
1444 );
1445 }
1446
1447 #[test]
1448 fn test_watch_registration() {
1449 let manager = StateManager::new().unwrap();
1450
1451 let devices = vec![Device {
1452 id: "RINCON_123".to_string(),
1453 name: "Living Room".to_string(),
1454 room_name: "Living Room".to_string(),
1455 ip_address: "192.168.1.100".to_string(),
1456 port: 1400,
1457 model_name: "Sonos One".to_string(),
1458 }];
1459 manager.add_devices(devices).unwrap();
1460
1461 let speaker_id = SpeakerId::new("RINCON_123");
1462
1463 assert!(!manager.is_watched(&speaker_id, "volume"));
1465
1466 manager.register_watch(&speaker_id, "volume");
1468 assert!(manager.is_watched(&speaker_id, "volume"));
1469
1470 manager.unregister_watch(&speaker_id, "volume");
1472 assert!(!manager.is_watched(&speaker_id, "volume"));
1473 }
1474
1475 #[test]
1476 fn test_change_event_emission() {
1477 let manager = StateManager::new().unwrap();
1478
1479 let devices = vec![Device {
1480 id: "RINCON_123".to_string(),
1481 name: "Living Room".to_string(),
1482 room_name: "Living Room".to_string(),
1483 ip_address: "192.168.1.100".to_string(),
1484 port: 1400,
1485 model_name: "Sonos One".to_string(),
1486 }];
1487 manager.add_devices(devices).unwrap();
1488
1489 let speaker_id = SpeakerId::new("RINCON_123");
1490
1491 manager.register_watch(&speaker_id, "volume");
1493
1494 let iter = manager.iter();
1497
1498 manager.set_property(&speaker_id, Volume::new(75));
1500 let event = iter.recv_timeout(std::time::Duration::from_millis(100));
1501 assert!(event.is_some());
1502
1503 let event = event.unwrap();
1504 assert_eq!(event.speaker_id.as_str(), "RINCON_123");
1505 assert_eq!(event.property_key(), "volume");
1506 }
1507
1508 #[test]
1509 fn test_set_group_property_emits_change_event() {
1510 let manager = StateManager::new().unwrap();
1511
1512 let devices = vec![Device {
1513 id: "RINCON_123".to_string(),
1514 name: "Living Room".to_string(),
1515 room_name: "Living Room".to_string(),
1516 ip_address: "192.168.1.100".to_string(),
1517 port: 1400,
1518 model_name: "Sonos One".to_string(),
1519 }];
1520 manager.add_devices(devices).unwrap();
1521
1522 let speaker_id = SpeakerId::new("RINCON_123");
1523 let group_id = GroupId::new("RINCON_123:1");
1524
1525 {
1527 let mut store = manager.store.write();
1528 store.add_group(GroupInfo::new(
1529 group_id.clone(),
1530 speaker_id.clone(),
1531 vec![speaker_id.clone()],
1532 ));
1533 }
1534
1535 manager.register_watch(&speaker_id, "group_volume");
1537
1538 let iter = manager.iter();
1539
1540 manager.set_group_property(&group_id, GroupVolume::new(80));
1542
1543 let event = iter.recv_timeout(std::time::Duration::from_millis(100));
1545 assert!(event.is_some());
1546
1547 let event = event.unwrap();
1548 assert_eq!(event.speaker_id.as_str(), "RINCON_123");
1549 assert_eq!(event.property_key(), "group_volume");
1550 assert_eq!(event.service(), Service::GroupRenderingControl);
1551 }
1552
1553 #[test]
1554 fn test_set_group_property_no_event_when_unwatched() {
1555 let manager = StateManager::new().unwrap();
1556
1557 let devices = vec![Device {
1558 id: "RINCON_123".to_string(),
1559 name: "Living Room".to_string(),
1560 room_name: "Living Room".to_string(),
1561 ip_address: "192.168.1.100".to_string(),
1562 port: 1400,
1563 model_name: "Sonos One".to_string(),
1564 }];
1565 manager.add_devices(devices).unwrap();
1566
1567 let speaker_id = SpeakerId::new("RINCON_123");
1568 let group_id = GroupId::new("RINCON_123:1");
1569
1570 {
1571 let mut store = manager.store.write();
1572 store.add_group(GroupInfo::new(
1573 group_id.clone(),
1574 speaker_id.clone(),
1575 vec![speaker_id.clone()],
1576 ));
1577 }
1578
1579 let iter = manager.iter();
1580
1581 manager.set_group_property(&group_id, GroupVolume::new(50));
1583 let event = iter.recv_timeout(std::time::Duration::from_millis(100));
1584 assert!(event.is_none());
1585 }
1586
1587 #[test]
1592 fn test_add_group_updates_speaker_to_group() {
1593 let mut store = StateStore::new();
1594
1595 let speaker1 = SpeakerId::new("RINCON_111");
1596 let speaker2 = SpeakerId::new("RINCON_222");
1597 let group_id = GroupId::new("RINCON_111:1");
1598
1599 let group = GroupInfo::new(
1600 group_id.clone(),
1601 speaker1.clone(),
1602 vec![speaker1.clone(), speaker2.clone()],
1603 );
1604
1605 store.add_group(group);
1606
1607 assert_eq!(store.speaker_to_group.get(&speaker1), Some(&group_id));
1609 assert_eq!(store.speaker_to_group.get(&speaker2), Some(&group_id));
1610 }
1611
1612 #[test]
1613 fn test_add_group_single_speaker() {
1614 let mut store = StateStore::new();
1615
1616 let speaker = SpeakerId::new("RINCON_333");
1617 let group_id = GroupId::new("RINCON_333:1");
1618
1619 let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
1620
1621 store.add_group(group.clone());
1622
1623 assert_eq!(store.speaker_to_group.get(&speaker), Some(&group_id));
1625
1626 assert_eq!(store.groups.get(&group_id), Some(&group));
1628 }
1629
1630 #[test]
1631 fn test_get_group_for_speaker_returns_correct_group() {
1632 let mut store = StateStore::new();
1633
1634 let speaker1 = SpeakerId::new("RINCON_111");
1635 let speaker2 = SpeakerId::new("RINCON_222");
1636 let speaker3 = SpeakerId::new("RINCON_333");
1637 let group1_id = GroupId::new("RINCON_111:1");
1638 let group2_id = GroupId::new("RINCON_333:1");
1639
1640 let group1 = GroupInfo::new(
1642 group1_id.clone(),
1643 speaker1.clone(),
1644 vec![speaker1.clone(), speaker2.clone()],
1645 );
1646
1647 let group2 = GroupInfo::new(group2_id.clone(), speaker3.clone(), vec![speaker3.clone()]);
1649
1650 store.add_group(group1.clone());
1651 store.add_group(group2.clone());
1652
1653 assert_eq!(store.get_group_for_speaker(&speaker1), Some(&group1));
1655 assert_eq!(store.get_group_for_speaker(&speaker2), Some(&group1));
1656 assert_eq!(store.get_group_for_speaker(&speaker3), Some(&group2));
1657 }
1658
1659 #[test]
1660 fn test_get_group_for_speaker_returns_none_for_unknown() {
1661 let store = StateStore::new();
1662
1663 let unknown_speaker = SpeakerId::new("RINCON_UNKNOWN");
1664
1665 assert!(store.get_group_for_speaker(&unknown_speaker).is_none());
1666 }
1667
1668 #[test]
1669 fn test_clear_groups_removes_all_group_data() {
1670 let mut store = StateStore::new();
1671
1672 let speaker1 = SpeakerId::new("RINCON_111");
1673 let speaker2 = SpeakerId::new("RINCON_222");
1674 let group_id = GroupId::new("RINCON_111:1");
1675
1676 let group = GroupInfo::new(
1677 group_id.clone(),
1678 speaker1.clone(),
1679 vec![speaker1.clone(), speaker2.clone()],
1680 );
1681
1682 store.add_group(group);
1683
1684 assert!(!store.groups.is_empty());
1686 assert!(!store.speaker_to_group.is_empty());
1687
1688 store.clear_groups();
1690
1691 assert!(store.groups.is_empty());
1693 assert!(store.group_props.is_empty());
1694 assert!(store.speaker_to_group.is_empty());
1695 }
1696
1697 #[test]
1698 fn test_clear_groups_then_add_new_groups() {
1699 let mut store = StateStore::new();
1700
1701 let speaker1 = SpeakerId::new("RINCON_111");
1703 let group1_id = GroupId::new("RINCON_111:1");
1704 let group1 = GroupInfo::new(group1_id.clone(), speaker1.clone(), vec![speaker1.clone()]);
1705 store.add_group(group1);
1706
1707 store.clear_groups();
1709
1710 let speaker2 = SpeakerId::new("RINCON_222");
1711 let group2_id = GroupId::new("RINCON_222:1");
1712 let group2 = GroupInfo::new(group2_id.clone(), speaker2.clone(), vec![speaker2.clone()]);
1713 store.add_group(group2.clone());
1714
1715 assert!(!store.groups.contains_key(&group1_id));
1717 assert_eq!(store.groups.get(&group2_id), Some(&group2));
1718
1719 assert!(!store.speaker_to_group.contains_key(&speaker1));
1721 assert_eq!(store.speaker_to_group.get(&speaker2), Some(&group2_id));
1722 }
1723
1724 #[test]
1729 fn test_state_manager_groups_returns_all_groups() {
1730 let manager = StateManager::new().unwrap();
1731
1732 let devices = vec![
1734 Device {
1735 id: "RINCON_111".to_string(),
1736 name: "Living Room".to_string(),
1737 room_name: "Living Room".to_string(),
1738 ip_address: "192.168.1.100".to_string(),
1739 port: 1400,
1740 model_name: "Sonos One".to_string(),
1741 },
1742 Device {
1743 id: "RINCON_222".to_string(),
1744 name: "Kitchen".to_string(),
1745 room_name: "Kitchen".to_string(),
1746 ip_address: "192.168.1.101".to_string(),
1747 port: 1400,
1748 model_name: "Sonos One".to_string(),
1749 },
1750 ];
1751 manager.add_devices(devices).unwrap();
1752
1753 let speaker1 = SpeakerId::new("RINCON_111");
1755 let speaker2 = SpeakerId::new("RINCON_222");
1756 let group1 = GroupInfo::new(
1757 GroupId::new("RINCON_111:1"),
1758 speaker1.clone(),
1759 vec![speaker1.clone()],
1760 );
1761 let group2 = GroupInfo::new(
1762 GroupId::new("RINCON_222:1"),
1763 speaker2.clone(),
1764 vec![speaker2.clone()],
1765 );
1766
1767 let topology = Topology::new(
1768 manager.speaker_infos(),
1769 vec![group1.clone(), group2.clone()],
1770 );
1771 manager.initialize(topology);
1772
1773 let groups = manager.groups();
1775 assert_eq!(groups.len(), 2);
1776
1777 let group_ids: Vec<_> = groups.iter().map(|g| g.id.clone()).collect();
1779 assert!(group_ids.contains(&GroupId::new("RINCON_111:1")));
1780 assert!(group_ids.contains(&GroupId::new("RINCON_222:1")));
1781 }
1782
1783 #[test]
1784 fn test_state_manager_groups_returns_empty_when_no_groups() {
1785 let manager = StateManager::new().unwrap();
1786
1787 let groups = manager.groups();
1789 assert!(groups.is_empty());
1790 }
1791
1792 #[test]
1793 fn test_state_manager_get_group_returns_correct_group() {
1794 let manager = StateManager::new().unwrap();
1795
1796 let devices = vec![Device {
1798 id: "RINCON_111".to_string(),
1799 name: "Living Room".to_string(),
1800 room_name: "Living Room".to_string(),
1801 ip_address: "192.168.1.100".to_string(),
1802 port: 1400,
1803 model_name: "Sonos One".to_string(),
1804 }];
1805 manager.add_devices(devices).unwrap();
1806
1807 let speaker = SpeakerId::new("RINCON_111");
1809 let group_id = GroupId::new("RINCON_111:1");
1810 let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
1811
1812 let topology = Topology::new(manager.speaker_infos(), vec![group.clone()]);
1813 manager.initialize(topology);
1814
1815 let found = manager.get_group(&group_id);
1817 assert!(found.is_some());
1818 assert_eq!(found.unwrap(), group);
1819 }
1820
1821 #[test]
1822 fn test_state_manager_get_group_returns_none_for_unknown() {
1823 let manager = StateManager::new().unwrap();
1824
1825 let unknown_id = GroupId::new("RINCON_UNKNOWN:1");
1827 let found = manager.get_group(&unknown_id);
1828 assert!(found.is_none());
1829 }
1830
1831 #[test]
1832 fn test_state_manager_get_group_for_speaker_returns_correct_group() {
1833 let manager = StateManager::new().unwrap();
1834
1835 let devices = vec![
1837 Device {
1838 id: "RINCON_111".to_string(),
1839 name: "Living Room".to_string(),
1840 room_name: "Living Room".to_string(),
1841 ip_address: "192.168.1.100".to_string(),
1842 port: 1400,
1843 model_name: "Sonos One".to_string(),
1844 },
1845 Device {
1846 id: "RINCON_222".to_string(),
1847 name: "Kitchen".to_string(),
1848 room_name: "Kitchen".to_string(),
1849 ip_address: "192.168.1.101".to_string(),
1850 port: 1400,
1851 model_name: "Sonos One".to_string(),
1852 },
1853 ];
1854 manager.add_devices(devices).unwrap();
1855
1856 let speaker1 = SpeakerId::new("RINCON_111");
1858 let speaker2 = SpeakerId::new("RINCON_222");
1859 let group_id = GroupId::new("RINCON_111:1");
1860 let group = GroupInfo::new(
1861 group_id.clone(),
1862 speaker1.clone(),
1863 vec![speaker1.clone(), speaker2.clone()],
1864 );
1865
1866 let topology = Topology::new(manager.speaker_infos(), vec![group.clone()]);
1867 manager.initialize(topology);
1868
1869 let found1 = manager.get_group_for_speaker(&speaker1);
1871 assert!(found1.is_some());
1872 assert_eq!(found1.unwrap(), group);
1873
1874 let found2 = manager.get_group_for_speaker(&speaker2);
1875 assert!(found2.is_some());
1876 assert_eq!(found2.unwrap(), group);
1877 }
1878
1879 #[test]
1880 fn test_state_manager_get_group_for_speaker_returns_none_for_unknown() {
1881 let manager = StateManager::new().unwrap();
1882
1883 let unknown_speaker = SpeakerId::new("RINCON_UNKNOWN");
1885 let found = manager.get_group_for_speaker(&unknown_speaker);
1886 assert!(found.is_none());
1887 }
1888
1889 #[test]
1890 fn test_state_manager_group_methods_consistency() {
1891 let manager = StateManager::new().unwrap();
1892
1893 let devices = vec![Device {
1895 id: "RINCON_111".to_string(),
1896 name: "Living Room".to_string(),
1897 room_name: "Living Room".to_string(),
1898 ip_address: "192.168.1.100".to_string(),
1899 port: 1400,
1900 model_name: "Sonos One".to_string(),
1901 }];
1902 manager.add_devices(devices).unwrap();
1903
1904 let speaker = SpeakerId::new("RINCON_111");
1906 let group_id = GroupId::new("RINCON_111:1");
1907 let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
1908
1909 let topology = Topology::new(manager.speaker_infos(), vec![group.clone()]);
1910 manager.initialize(topology);
1911
1912 let groups = manager.groups();
1914 assert_eq!(groups.len(), 1);
1915 assert_eq!(groups[0], group);
1916
1917 let by_id = manager.get_group(&group_id);
1918 assert_eq!(by_id, Some(group.clone()));
1919
1920 let by_speaker = manager.get_group_for_speaker(&speaker);
1921 assert_eq!(by_speaker, Some(group.clone()));
1922
1923 assert_eq!(groups[0], by_id.unwrap());
1925 assert_eq!(groups[0], by_speaker.unwrap());
1926 }
1927
1928 #[test]
1933 fn test_get_boot_seq_returns_none_for_unknown_speaker() {
1934 let manager = StateManager::new().unwrap();
1935 let unknown = SpeakerId::new("RINCON_UNKNOWN");
1936 assert!(manager.get_boot_seq(&unknown).is_none());
1937 }
1938
1939 #[test]
1940 fn test_boot_seq_defaults_to_zero_for_new_speaker() {
1941 let manager = StateManager::new().unwrap();
1942
1943 let devices = vec![Device {
1944 id: "RINCON_123".to_string(),
1945 name: "Living Room".to_string(),
1946 room_name: "Living Room".to_string(),
1947 ip_address: "192.168.1.100".to_string(),
1948 port: 1400,
1949 model_name: "Sonos One".to_string(),
1950 }];
1951 manager.add_devices(devices).unwrap();
1952
1953 let speaker_id = SpeakerId::new("RINCON_123");
1954
1955 assert_eq!(manager.get_boot_seq(&speaker_id), Some(0));
1957 }
1958
1959 #[test]
1964 fn test_state_watch_registry_register_and_unregister() {
1965 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1966 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
1967 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
1968
1969 let ip: IpAddr = "192.168.1.100".parse().unwrap();
1970 let speaker_id = SpeakerId::new("RINCON_123");
1971 ip_to_speaker.write().insert(ip, speaker_id.clone());
1972
1973 let registry = StateWatchRegistry {
1974 watched: Arc::clone(&watched),
1975 ip_to_speaker: Arc::clone(&ip_to_speaker),
1976 key_to_service: Arc::clone(&key_to_service),
1977 };
1978
1979 registry.register_watch(&speaker_id, "volume", Service::RenderingControl);
1981 registry.register_watch(&speaker_id, "mute", Service::RenderingControl);
1982 registry.register_watch(&speaker_id, "playback_state", Service::AVTransport);
1983
1984 assert_eq!(watched.read().len(), 3);
1985
1986 registry.unregister_watches_for_service(ip, Service::RenderingControl);
1988
1989 let w = watched.read();
1990 assert_eq!(w.len(), 1);
1991 assert!(is_pair_watched(&w, &speaker_id, "playback_state"));
1992 assert!(!is_pair_watched(&w, &speaker_id, "volume"));
1993 assert!(!is_pair_watched(&w, &speaker_id, "mute"));
1994 }
1995
1996 #[test]
1997 fn test_state_watch_registry_unknown_ip_is_noop() {
1998 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1999 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
2000 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
2001
2002 let speaker_id = SpeakerId::new("RINCON_123");
2003
2004 let registry = StateWatchRegistry {
2005 watched: Arc::clone(&watched),
2006 ip_to_speaker,
2007 key_to_service: Arc::clone(&key_to_service),
2008 };
2009
2010 retain_direct_watch(&watched, &speaker_id, "volume");
2012 key_to_service
2013 .write()
2014 .insert("volume", Service::RenderingControl);
2015
2016 let unknown_ip: IpAddr = "10.0.0.1".parse().unwrap();
2018 registry.unregister_watches_for_service(unknown_ip, Service::RenderingControl);
2019
2020 assert_eq!(watched.read().len(), 1);
2022 }
2023
2024 #[test]
2025 fn test_state_watch_registry_only_removes_matching_speaker() {
2026 let watched = Arc::new(RwLock::new(WatchCounts::new()));
2027 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
2028 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
2029
2030 let ip1: IpAddr = "192.168.1.100".parse().unwrap();
2031 let ip2: IpAddr = "192.168.1.101".parse().unwrap();
2032 let speaker1 = SpeakerId::new("RINCON_111");
2033 let speaker2 = SpeakerId::new("RINCON_222");
2034
2035 ip_to_speaker.write().insert(ip1, speaker1.clone());
2036 ip_to_speaker.write().insert(ip2, speaker2.clone());
2037
2038 let registry = StateWatchRegistry {
2039 watched: Arc::clone(&watched),
2040 ip_to_speaker,
2041 key_to_service: Arc::clone(&key_to_service),
2042 };
2043
2044 registry.register_watch(&speaker1, "volume", Service::RenderingControl);
2046 registry.register_watch(&speaker2, "volume", Service::RenderingControl);
2047 assert_eq!(watched.read().len(), 2);
2048
2049 registry.unregister_watches_for_service(ip1, Service::RenderingControl);
2051
2052 let w = watched.read();
2053 assert_eq!(w.len(), 1);
2054 assert!(is_pair_watched(&w, &speaker2, "volume"));
2055 assert!(!is_pair_watched(&w, &speaker1, "volume"));
2056 }
2057
2058 #[test]
2069 fn test_watch_refcount_survives_partial_release() {
2070 let manager = StateManager::new().unwrap();
2071 let speaker_id = SpeakerId::new("RINCON_123");
2072
2073 manager.register_watch(&speaker_id, "volume");
2074 manager.register_watch(&speaker_id, "volume");
2075 assert!(manager.is_watched(&speaker_id, "volume"));
2076
2077 manager.unregister_watch(&speaker_id, "volume");
2079 assert!(
2080 manager.is_watched(&speaker_id, "volume"),
2081 "one of two watchers released — the property must stay watched"
2082 );
2083
2084 manager.unregister_watch(&speaker_id, "volume");
2086 assert!(!manager.is_watched(&speaker_id, "volume"));
2087
2088 manager.unregister_watch(&speaker_id, "volume");
2090 assert!(!manager.is_watched(&speaker_id, "volume"));
2091 }
2092
2093 #[test]
2101 fn test_service_unregister_keeps_directly_held_watches() {
2102 let watched = Arc::new(RwLock::new(WatchCounts::new()));
2103 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
2104 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
2105
2106 let ip: IpAddr = "192.168.1.100".parse().unwrap();
2107 let speaker_id = SpeakerId::new("RINCON_123");
2108 ip_to_speaker.write().insert(ip, speaker_id.clone());
2109
2110 let registry = StateWatchRegistry {
2111 watched: Arc::clone(&watched),
2112 ip_to_speaker,
2113 key_to_service,
2114 };
2115
2116 registry.register_watch(&speaker_id, "volume", Service::RenderingControl);
2118 retain_direct_watch(&watched, &speaker_id, "volume");
2119 registry.register_watch(&speaker_id, "mute", Service::RenderingControl);
2121 retain_direct_watch(&watched, &speaker_id, "mute");
2122
2123 registry.unregister_watches_for_service(ip, Service::RenderingControl);
2124
2125 let w = watched.read();
2126 assert!(
2127 is_pair_watched(&w, &speaker_id, "volume"),
2128 "the direct hold on volume must survive the subscription teardown"
2129 );
2130 assert!(
2131 is_pair_watched(&w, &speaker_id, "mute"),
2132 "the direct hold on mute must survive the subscription teardown"
2133 );
2134 }
2135
2136 #[test]
2148 fn test_set_property_on_group_member_is_readable_from_both() {
2149 let manager = StateManager::new().unwrap();
2150
2151 let devices = vec![
2152 Device {
2153 id: "RINCON_COORD".to_string(),
2154 name: "Living Room".to_string(),
2155 room_name: "Living Room".to_string(),
2156 ip_address: "192.168.1.100".to_string(),
2157 port: 1400,
2158 model_name: "Sonos One".to_string(),
2159 },
2160 Device {
2161 id: "RINCON_MEMBER".to_string(),
2162 name: "Kitchen".to_string(),
2163 room_name: "Kitchen".to_string(),
2164 ip_address: "192.168.1.101".to_string(),
2165 port: 1400,
2166 model_name: "Sonos One".to_string(),
2167 },
2168 ];
2169 manager.add_devices(devices).unwrap();
2170
2171 let coordinator = SpeakerId::new("RINCON_COORD");
2172 let member = SpeakerId::new("RINCON_MEMBER");
2173 let group_id = GroupId::new("RINCON_COORD:1");
2174 let topology = Topology::new(
2175 manager.speaker_infos(),
2176 vec![GroupInfo::new(
2177 group_id,
2178 coordinator.clone(),
2179 vec![coordinator.clone(), member.clone()],
2180 )],
2181 );
2182 manager.initialize(topology);
2183
2184 manager.set_property(&member, PlaybackState::Playing);
2187
2188 assert_eq!(
2189 manager.get_property::<PlaybackState>(&member),
2190 Some(PlaybackState::Playing),
2191 "the member must be able to read back what it just wrote"
2192 );
2193 assert_eq!(
2194 manager.get_property::<PlaybackState>(&coordinator),
2195 Some(PlaybackState::Playing),
2196 "the write belongs in the coordinator's bag, which is where reads resolve"
2197 );
2198
2199 manager.set_property(&member, Volume::new(33));
2201 assert_eq!(
2202 manager.get_property::<Volume>(&member),
2203 Some(Volume::new(33))
2204 );
2205 assert_eq!(
2206 manager.get_property::<Volume>(&coordinator),
2207 None,
2208 "PerSpeaker writes must not be redirected to the coordinator"
2209 );
2210 }
2211
2212 #[test]
2217 fn test_resolve_coordinator_for_standalone_speaker() {
2218 let mut store = StateStore::new();
2219
2220 let speaker = SpeakerId::new("RINCON_111");
2221 let group_id = GroupId::new("RINCON_111:1");
2222
2223 store.add_speaker(SpeakerInfo {
2224 id: speaker.clone(),
2225 name: "Living Room".to_string(),
2226 room_name: "Living Room".to_string(),
2227 ip_address: "192.168.1.100".parse().unwrap(),
2228 port: 1400,
2229 model_name: "Test".to_string(),
2230 software_version: "1.0".to_string(),
2231 boot_seq: 0,
2232 satellites: vec![],
2233 });
2234 store.add_group(GroupInfo::new(
2235 group_id,
2236 speaker.clone(),
2237 vec![speaker.clone()],
2238 ));
2239
2240 assert_eq!(store.resolve_coordinator(&speaker), speaker);
2242 }
2243
2244 #[test]
2245 fn test_resolve_coordinator_for_group_member() {
2246 let mut store = StateStore::new();
2247
2248 let coordinator = SpeakerId::new("RINCON_COORD");
2249 let member = SpeakerId::new("RINCON_MEMBER");
2250 let group_id = GroupId::new("RINCON_COORD:1");
2251
2252 store.add_group(GroupInfo::new(
2253 group_id,
2254 coordinator.clone(),
2255 vec![coordinator.clone(), member.clone()],
2256 ));
2257
2258 assert_eq!(store.resolve_coordinator(&member), coordinator);
2260 assert_eq!(store.resolve_coordinator(&coordinator), coordinator);
2262 }
2263
2264 #[test]
2265 fn test_resolve_coordinator_no_group_data() {
2266 let store = StateStore::new();
2267
2268 let speaker = SpeakerId::new("RINCON_UNKNOWN");
2269
2270 assert_eq!(store.resolve_coordinator(&speaker), speaker);
2272 }
2273
2274 #[test]
2279 fn test_get_resolved_per_coordinator_reads_from_coordinator() {
2280 let mut store = StateStore::new();
2281
2282 let coordinator = SpeakerId::new("RINCON_COORD");
2283 let member = SpeakerId::new("RINCON_MEMBER");
2284 let group_id = GroupId::new("RINCON_COORD:1");
2285
2286 store.add_speaker(SpeakerInfo {
2287 id: coordinator.clone(),
2288 name: "Coord".to_string(),
2289 room_name: "Coord".to_string(),
2290 ip_address: "192.168.1.100".parse().unwrap(),
2291 port: 1400,
2292 model_name: "Test".to_string(),
2293 software_version: "1.0".to_string(),
2294 boot_seq: 0,
2295 satellites: vec![],
2296 });
2297 store.add_speaker(SpeakerInfo {
2298 id: member.clone(),
2299 name: "Member".to_string(),
2300 room_name: "Member".to_string(),
2301 ip_address: "192.168.1.101".parse().unwrap(),
2302 port: 1400,
2303 model_name: "Test".to_string(),
2304 software_version: "1.0".to_string(),
2305 boot_seq: 0,
2306 satellites: vec![],
2307 });
2308 store.add_group(GroupInfo::new(
2309 group_id,
2310 coordinator.clone(),
2311 vec![coordinator.clone(), member.clone()],
2312 ));
2313
2314 store.set(&coordinator, PlaybackState::Playing, test_stamp());
2316
2317 let resolved: Option<PlaybackState> = store.get_resolved(&member);
2319 assert_eq!(resolved, Some(PlaybackState::Playing));
2320
2321 let direct: Option<PlaybackState> = store.get(&member);
2323 assert_eq!(direct, None);
2324 }
2325
2326 #[test]
2327 fn test_get_resolved_per_speaker_reads_own_props() {
2328 let mut store = StateStore::new();
2329
2330 let coordinator = SpeakerId::new("RINCON_COORD");
2331 let member = SpeakerId::new("RINCON_MEMBER");
2332 let group_id = GroupId::new("RINCON_COORD:1");
2333
2334 store.add_speaker(SpeakerInfo {
2335 id: coordinator.clone(),
2336 name: "Coord".to_string(),
2337 room_name: "Coord".to_string(),
2338 ip_address: "192.168.1.100".parse().unwrap(),
2339 port: 1400,
2340 model_name: "Test".to_string(),
2341 software_version: "1.0".to_string(),
2342 boot_seq: 0,
2343 satellites: vec![],
2344 });
2345 store.add_speaker(SpeakerInfo {
2346 id: member.clone(),
2347 name: "Member".to_string(),
2348 room_name: "Member".to_string(),
2349 ip_address: "192.168.1.101".parse().unwrap(),
2350 port: 1400,
2351 model_name: "Test".to_string(),
2352 software_version: "1.0".to_string(),
2353 boot_seq: 0,
2354 satellites: vec![],
2355 });
2356 store.add_group(GroupInfo::new(
2357 group_id,
2358 coordinator.clone(),
2359 vec![coordinator.clone(), member.clone()],
2360 ));
2361
2362 store.set(&coordinator, Volume::new(80), test_stamp());
2364
2365 let resolved: Option<Volume> = store.get_resolved(&member);
2367 assert_eq!(resolved, None);
2368
2369 let coord_resolved: Option<Volume> = store.get_resolved(&coordinator);
2371 assert_eq!(coord_resolved, Some(Volume::new(80)));
2372 }
2373
2374 #[test]
2375 fn test_update_speaker_ip() {
2376 let manager = StateManager::new().unwrap();
2377
2378 let devices = vec![Device {
2379 id: "RINCON_111".to_string(),
2380 name: "Office".to_string(),
2381 room_name: "Office".to_string(),
2382 ip_address: "192.168.4.198".to_string(),
2383 port: 1400,
2384 model_name: "Roam 2".to_string(),
2385 }];
2386 manager.add_devices(devices).unwrap();
2387
2388 let speaker_id = SpeakerId::new("RINCON_111");
2389 let old_ip: IpAddr = "192.168.4.198".parse().unwrap();
2390 let new_ip: IpAddr = "192.168.4.200".parse().unwrap();
2391
2392 assert_eq!(manager.get_speaker_ip(&speaker_id), Some(old_ip));
2394
2395 manager.update_speaker_ip(&speaker_id, new_ip);
2397
2398 assert_eq!(manager.get_speaker_ip(&speaker_id), Some(new_ip));
2400
2401 let ip_map = manager.ip_to_speaker.read();
2403 assert!(!ip_map.contains_key(&old_ip));
2404 assert_eq!(ip_map.get(&new_ip), Some(&speaker_id));
2405 }
2406
2407 #[test]
2408 fn test_update_speaker_ip_no_change() {
2409 let manager = StateManager::new().unwrap();
2410
2411 let devices = vec![Device {
2412 id: "RINCON_111".to_string(),
2413 name: "Office".to_string(),
2414 room_name: "Office".to_string(),
2415 ip_address: "192.168.4.198".to_string(),
2416 port: 1400,
2417 model_name: "Roam 2".to_string(),
2418 }];
2419 manager.add_devices(devices).unwrap();
2420
2421 let speaker_id = SpeakerId::new("RINCON_111");
2422 let same_ip: IpAddr = "192.168.4.198".parse().unwrap();
2423
2424 manager.update_speaker_ip(&speaker_id, same_ip);
2426 assert_eq!(manager.get_speaker_ip(&speaker_id), Some(same_ip));
2427 }
2428
2429 #[test]
2430 fn test_satellite_ids() {
2431 let manager = StateManager::new().unwrap();
2432
2433 assert!(manager.get_satellite_ids().is_empty());
2434
2435 let ids = vec![SpeakerId::new("RINCON_SAT1"), SpeakerId::new("RINCON_SAT2")];
2436 manager.set_satellite_ids(ids.clone());
2437
2438 let stored = manager.get_satellite_ids();
2439 assert_eq!(stored.len(), 2);
2440 assert!(stored.contains(&SpeakerId::new("RINCON_SAT1")));
2441 assert!(stored.contains(&SpeakerId::new("RINCON_SAT2")));
2442 }
2443
2444 #[test]
2460 fn test_queued_events_preserve_every_intermediate_value() {
2461 let manager = StateManager::new().unwrap();
2462 manager
2463 .add_devices(vec![Device {
2464 id: "RINCON_QUEUE".to_string(),
2465 name: "Queue Test".to_string(),
2466 room_name: "Test".to_string(),
2467 ip_address: "192.0.2.10".to_string(),
2468 port: 1400,
2469 model_name: "Sonos One".to_string(),
2470 }])
2471 .unwrap();
2472
2473 let speaker_id = SpeakerId::new("RINCON_QUEUE");
2474 manager.register_watch(&speaker_id, PlaybackState::KEY);
2475
2476 let iter = manager.iter();
2477
2478 manager.set_property(&speaker_id, PlaybackState::Playing);
2481 manager.set_property(&speaker_id, PlaybackState::Transitioning);
2482 manager.set_property(&speaker_id, PlaybackState::Playing);
2483 let observed: Vec<PlaybackState> = iter
2484 .try_iter()
2485 .filter_map(|e| match e.change {
2486 PropertyChange::PlaybackState(s) => Some(s),
2487 _ => None,
2488 })
2489 .collect();
2490
2491 assert_eq!(
2492 observed,
2493 vec![
2494 PlaybackState::Playing,
2495 PlaybackState::Transitioning,
2496 PlaybackState::Playing,
2497 ],
2498 "every queued value must be observable from the event stream"
2499 );
2500
2501 assert_eq!(
2504 manager.get_property::<PlaybackState>(&speaker_id),
2505 Some(PlaybackState::Playing)
2506 );
2507 }
2508
2509 fn manager_watching_volume() -> (StateManager, SpeakerId) {
2511 let manager = StateManager::new().unwrap();
2512 manager
2513 .add_devices(vec![Device {
2514 id: "RINCON_FANOUT".to_string(),
2515 name: "Living Room".to_string(),
2516 room_name: "Living Room".to_string(),
2517 ip_address: "192.0.2.10".to_string(),
2519 port: 1400,
2520 model_name: "Sonos One".to_string(),
2521 }])
2522 .unwrap();
2523 let speaker_id = SpeakerId::new("RINCON_FANOUT");
2524 manager.register_watch(&speaker_id, Volume::KEY);
2525 (manager, speaker_id)
2526 }
2527
2528 fn volumes_from(iter: &ChangeIterator) -> Vec<u8> {
2529 iter.try_iter()
2530 .filter_map(|e| match e.change {
2531 PropertyChange::Volume(v) => Some(v.value()),
2532 _ => None,
2533 })
2534 .collect()
2535 }
2536
2537 #[test]
2545 fn test_two_iterators_each_receive_every_event() {
2546 let (manager, speaker_id) = manager_watching_volume();
2547
2548 let dashboard = manager.iter();
2549 let logger = manager.iter();
2550
2551 for v in [10u8, 20, 30, 40] {
2552 manager.set_property(&speaker_id, Volume::new(v));
2553 }
2554
2555 assert_eq!(
2557 volumes_from(&dashboard),
2558 vec![10, 20, 30, 40],
2559 "the first iterator must see every event"
2560 );
2561 assert_eq!(
2562 volumes_from(&logger),
2563 vec![10, 20, 30, 40],
2564 "the second iterator must see every event too, not a subset"
2565 );
2566 }
2567
2568 #[test]
2572 fn test_single_iterator_receives_every_event_in_order() {
2573 let (manager, speaker_id) = manager_watching_volume();
2574
2575 let iter = manager.iter();
2576 let sent: Vec<u8> = (1..=25).collect();
2577 for &v in &sent {
2578 manager.set_property(&speaker_id, Volume::new(v));
2579 }
2580
2581 assert_eq!(volumes_from(&iter), sent);
2582 }
2583
2584 #[test]
2590 fn test_dropped_consumer_does_not_stall_survivor() {
2591 let (manager, speaker_id) = manager_watching_volume();
2592
2593 let survivor = manager.iter();
2594 let departing = manager.iter();
2595
2596 manager.set_property(&speaker_id, Volume::new(5));
2597 drop(departing);
2598
2599 manager.set_property(&speaker_id, Volume::new(6));
2601 manager.set_property(&speaker_id, Volume::new(7));
2602
2603 assert_eq!(
2604 volumes_from(&survivor),
2605 vec![5, 6, 7],
2606 "the remaining consumer must keep receiving after a sibling drops"
2607 );
2608
2609 let latecomer = manager.iter();
2611 manager.set_property(&speaker_id, Volume::new(8));
2612 assert_eq!(volumes_from(&latecomer), vec![8]);
2613 }
2614
2615 #[test]
2621 fn test_stale_fetch_does_not_clobber_newer_event_value() {
2622 let manager = StateManager::new().unwrap();
2623 manager
2624 .add_devices(vec![Device {
2625 id: "RINCON_RACE".to_string(),
2626 name: "Race Test".to_string(),
2627 room_name: "Test".to_string(),
2628 ip_address: "192.0.2.11".to_string(),
2629 port: 1400,
2630 model_name: "Sonos One".to_string(),
2631 }])
2632 .unwrap();
2633
2634 let speaker_id = SpeakerId::new("RINCON_RACE");
2635
2636 let fetch_observed_at = Instant::now();
2639
2640 let event_outcome = manager.set_property_stamped(
2642 &speaker_id,
2643 Volume::new(40),
2644 WriteStamp::now(ChangeSource::Event),
2645 );
2646 assert_eq!(event_outcome, WriteOutcome::Changed);
2647
2648 let fetch_outcome = manager.set_property_stamped(
2650 &speaker_id,
2651 Volume::new(10),
2652 WriteStamp::observed_at(ChangeSource::Fetch, fetch_observed_at),
2653 );
2654
2655 assert_eq!(
2656 fetch_outcome,
2657 WriteOutcome::Stale,
2658 "a fetch observed before the stored event must be rejected"
2659 );
2660 assert_eq!(
2661 manager.get_property::<Volume>(&speaker_id),
2662 Some(Volume::new(40)),
2663 "the newer event value must survive the late fetch response"
2664 );
2665 }
2666
2667 #[test]
2671 fn test_newer_write_is_accepted_after_an_earlier_one() {
2672 let manager = StateManager::new().unwrap();
2673 manager
2674 .add_devices(vec![Device {
2675 id: "RINCON_FWD".to_string(),
2676 name: "Forward Test".to_string(),
2677 room_name: "Test".to_string(),
2678 ip_address: "192.0.2.12".to_string(),
2679 port: 1400,
2680 model_name: "Sonos One".to_string(),
2681 }])
2682 .unwrap();
2683
2684 let speaker_id = SpeakerId::new("RINCON_FWD");
2685 let early = Instant::now();
2686
2687 assert_eq!(
2688 manager.set_property_stamped(
2689 &speaker_id,
2690 Volume::new(10),
2691 WriteStamp::observed_at(ChangeSource::Fetch, early),
2692 ),
2693 WriteOutcome::Changed
2694 );
2695
2696 assert_eq!(
2698 manager.set_property_stamped(
2699 &speaker_id,
2700 Volume::new(20),
2701 WriteStamp::now(ChangeSource::Fetch),
2702 ),
2703 WriteOutcome::Changed
2704 );
2705 assert_eq!(
2706 manager.get_property::<Volume>(&speaker_id),
2707 Some(Volume::new(20))
2708 );
2709 }
2710}