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 resolved = self.ip_to_speaker.read().get(&ip).cloned();
691 let speaker_id = match resolved {
692 Some(id) => id,
693 None => {
694 tracing::warn!(
695 "unregister_watches_for_service: no speaker found for IP {}",
696 ip
697 );
698 return;
699 }
700 };
701
702 let service_keys: Vec<&'static str> = self
704 .key_to_service
705 .read()
706 .iter()
707 .filter(|(_, &svc)| svc == service)
708 .map(|(&key, _)| key)
709 .collect();
710
711 for key in service_keys {
718 release_subscription_watch(&self.watched, &speaker_id, key);
719 }
720 }
721}
722
723impl StateManager {
724 pub fn new() -> Result<Self> {
732 Self::builder().build()
733 }
734
735 pub fn builder() -> StateManagerBuilder {
737 StateManagerBuilder::default()
738 }
739
740 pub fn add_devices(&self, devices: Vec<Device>) -> Result<()> {
749 let mut store = self.store.write();
750 let mut ip_map = self.ip_to_speaker.write();
751
752 for device in devices {
753 let speaker_id = SpeakerId::new(&device.id);
754 let ip: IpAddr = device
755 .ip_address
756 .parse()
757 .map_err(|_| StateError::InvalidIpAddress(device.ip_address.clone()))?;
758
759 let friendly_name = if device.room_name.is_empty() || device.room_name == "Unknown" {
760 device.name.clone()
761 } else {
762 device.room_name.clone()
763 };
764
765 let info = SpeakerInfo {
766 id: speaker_id.clone(),
767 name: friendly_name,
768 room_name: device.room_name.clone(),
769 ip_address: ip,
770 port: device.port,
771 model_name: device.model_name.clone(),
772 software_version: "unknown".to_string(),
773 boot_seq: 0,
774 satellites: vec![],
775 };
776
777 ip_map.insert(ip, speaker_id.clone());
779 tracing::debug!(
780 "Added speaker {} at IP {} to ip_to_speaker map",
781 speaker_id.as_str(),
782 ip
783 );
784
785 store.add_speaker(info);
786 }
787
788 drop(store);
790 drop(ip_map);
791
792 if let Some(em) = self.event_manager.get() {
793 let devices_for_em: Vec<_> = self
794 .speaker_infos()
795 .iter()
796 .map(|info| sonos_discovery::Device {
797 id: info.id.as_str().to_string(),
798 name: info.name.clone(),
799 room_name: info.room_name.clone(),
800 ip_address: info.ip_address.to_string(),
801 port: info.port,
802 model_name: info.model_name.clone(),
803 })
804 .collect();
805
806 if let Err(e) = em.add_devices(devices_for_em) {
807 tracing::warn!("Failed to add devices to event manager: {}", e);
808 }
809 }
810
811 Ok(())
812 }
813
814 pub fn speaker_infos(&self) -> Vec<SpeakerInfo> {
816 self.store.read().speakers()
817 }
818
819 pub fn speaker_info(&self, speaker_id: &SpeakerId) -> Option<SpeakerInfo> {
821 self.store.read().speaker(speaker_id).cloned()
822 }
823
824 pub fn get_speaker_ip(&self, speaker_id: &SpeakerId) -> Option<IpAddr> {
826 self.store.read().speaker(speaker_id).map(|s| s.ip_address)
827 }
828
829 pub fn get_boot_seq(&self, speaker_id: &SpeakerId) -> Option<u32> {
831 self.store.read().speaker(speaker_id).map(|s| s.boot_seq)
832 }
833
834 pub fn update_speaker_ip(&self, speaker_id: &SpeakerId, new_ip: IpAddr) {
836 let old_ip = {
837 let mut store = self.store.write();
838 store.update_speaker_ip_address(speaker_id, new_ip)
839 };
840 if let Some(old_ip) = old_ip {
841 let mut map = self.ip_to_speaker.write();
842 map.remove(&old_ip);
843 map.insert(new_ip, speaker_id.clone());
844 }
845 }
846
847 pub fn get_satellite_ids(&self) -> Vec<SpeakerId> {
849 self.store.read().satellite_ids.iter().cloned().collect()
850 }
851
852 pub fn set_satellite_ids(&self, ids: Vec<SpeakerId>) {
854 self.store.write().satellite_ids = ids.into_iter().collect();
855 }
856
857 pub fn iter(&self) -> ChangeIterator {
884 ChangeIterator::new(&self.fanout)
885 }
886
887 pub fn get_property<P: SonosProperty>(&self, speaker_id: &SpeakerId) -> Option<P> {
892 self.store.read().get_resolved::<P>(speaker_id)
893 }
894
895 pub fn get_group_property<P: Property>(&self, group_id: &GroupId) -> Option<P> {
897 self.store.read().get_group::<P>(group_id)
898 }
899
900 pub fn set_property<P: SonosProperty>(&self, speaker_id: &SpeakerId, value: P) {
921 self.set_property_stamped(
922 speaker_id,
923 value,
924 WriteStamp::now(ChangeSource::LocalAction),
925 );
926 }
927
928 pub fn set_property_stamped<P: SonosProperty>(
934 &self,
935 speaker_id: &SpeakerId,
936 value: P,
937 stamp: WriteStamp,
938 ) -> WriteOutcome {
939 let (target_id, outcome) = {
943 let mut store = self.store.write();
944 let target_id = store.resolve_write_target::<P>(speaker_id);
945 let outcome = store.set::<P>(&target_id, value.clone(), stamp);
946 (target_id, outcome)
947 };
948
949 if outcome.changed() {
950 self.maybe_emit_change(speaker_id, &value, stamp);
953 if target_id != *speaker_id {
956 self.maybe_emit_change(&target_id, &value, stamp);
957 }
958 }
959
960 outcome
961 }
962
963 pub fn set_group_property<P: SonosProperty>(&self, group_id: &GroupId, value: P) {
972 self.set_group_property_stamped(
973 group_id,
974 value,
975 WriteStamp::now(ChangeSource::LocalAction),
976 );
977 }
978
979 pub fn set_group_property_stamped<P: SonosProperty>(
984 &self,
985 group_id: &GroupId,
986 value: P,
987 stamp: WriteStamp,
988 ) -> WriteOutcome {
989 let (outcome, coordinator_id) = {
990 let mut store = self.store.write();
991 let outcome = store.set_group::<P>(group_id, value.clone(), stamp);
992 if !outcome.changed() {
993 return outcome;
994 }
995 let coordinator_id = store.groups.get(group_id).map(|g| g.coordinator_id.clone());
996 (outcome, coordinator_id)
997 };
998
999 if let Some(coordinator_id) = coordinator_id {
1000 self.maybe_emit_change(&coordinator_id, &value, stamp);
1001 }
1002
1003 outcome
1004 }
1005
1006 pub fn register_watch(&self, speaker_id: &SpeakerId, property_key: &'static str) {
1011 retain_direct_watch(&self.watched, speaker_id, property_key);
1012 }
1013
1014 pub fn unregister_watch(&self, speaker_id: &SpeakerId, property_key: &'static str) {
1021 release_direct_watch(&self.watched, speaker_id, property_key);
1022 }
1023
1024 pub fn watch_property_with_subscription<P: SonosProperty>(
1032 &self,
1033 speaker_id: &SpeakerId,
1034 ) -> Result<Option<P>> {
1035 self.register_watch(speaker_id, P::KEY);
1037
1038 if let Some(em) = self.event_manager.get() {
1040 if let Some(ip) = self.get_speaker_ip(speaker_id) {
1042 if let Err(e) = em.ensure_service_subscribed(ip, P::SERVICE) {
1043 tracing::warn!(
1044 "Failed to subscribe to {:?} for {}: {}",
1045 P::SERVICE,
1046 speaker_id.as_str(),
1047 e
1048 );
1049 }
1050 }
1051 }
1052
1053 Ok(self.get_property::<P>(speaker_id))
1054 }
1055
1056 pub fn unwatch_property_with_subscription<P: SonosProperty>(&self, speaker_id: &SpeakerId) {
1058 self.unregister_watch(speaker_id, P::KEY);
1060
1061 if let Some(em) = self.event_manager.get() {
1063 if let Some(ip) = self.get_speaker_ip(speaker_id) {
1064 if let Err(e) = em.release_service_subscription(ip, P::SERVICE) {
1065 tracing::warn!(
1066 "Failed to unsubscribe from {:?} for {}: {}",
1067 P::SERVICE,
1068 speaker_id.as_str(),
1069 e
1070 );
1071 }
1072 }
1073 }
1074 }
1075
1076 pub fn is_watched(&self, speaker_id: &SpeakerId, property_key: &'static str) -> bool {
1078 is_pair_watched(&self.watched.read(), speaker_id, property_key)
1079 }
1080
1081 fn maybe_emit_change<P: SonosProperty>(
1083 &self,
1084 speaker_id: &SpeakerId,
1085 value: &P,
1086 stamp: WriteStamp,
1087 ) {
1088 let is_watched = is_pair_watched(&self.watched.read(), speaker_id, P::KEY);
1089 if !is_watched {
1090 return;
1091 }
1092
1093 let Some(change) = value.to_change() else {
1099 tracing::warn!(
1100 "Not emitting a change event for {}: no PropertyChange variant \
1101 (SonosProperty::to_change returned None)",
1102 P::KEY
1103 );
1104 return;
1105 };
1106
1107 self.fanout
1108 .send(ChangeEvent::new(speaker_id.clone(), change, stamp));
1109 }
1110
1111 pub fn initialize(&self, topology: Topology) {
1113 let mut store = self.store.write();
1114 for speaker in &topology.speakers {
1115 store.add_speaker(speaker.clone());
1116 }
1117 for group in &topology.groups {
1118 store.add_group(group.clone());
1119 }
1120 store.set_system(topology, WriteStamp::now(ChangeSource::LocalAction));
1124 }
1125
1126 pub fn is_initialized(&self) -> bool {
1128 !self.store.read().is_empty()
1129 }
1130
1131 pub fn speaker_count(&self) -> usize {
1133 self.store.read().speaker_count()
1134 }
1135
1136 pub fn group_count(&self) -> usize {
1138 self.store.read().group_count()
1139 }
1140
1141 pub fn groups(&self) -> Vec<GroupInfo> {
1146 self.store.read().groups.values().cloned().collect()
1147 }
1148
1149 pub fn get_group(&self, group_id: &GroupId) -> Option<GroupInfo> {
1151 self.store.read().groups.get(group_id).cloned()
1152 }
1153
1154 pub fn get_group_for_speaker(&self, speaker_id: &SpeakerId) -> Option<GroupInfo> {
1158 let store = self.store.read();
1159 let group_id = store.speaker_to_group.get(speaker_id)?;
1160 store.groups.get(group_id).cloned()
1161 }
1162
1163 pub fn resolve_subscription_target(
1171 &self,
1172 speaker_id: &SpeakerId,
1173 speaker_ip: IpAddr,
1174 service: Service,
1175 ) -> (SpeakerId, IpAddr) {
1176 if service.scope() == ServiceScope::PerCoordinator {
1177 let store = self.store.read();
1178 let coordinator_id = store.resolve_coordinator(speaker_id);
1179 if coordinator_id == *speaker_id {
1180 (speaker_id.clone(), speaker_ip)
1181 } else {
1182 let coord_ip = store
1183 .speaker(&coordinator_id)
1184 .map(|s| s.ip_address)
1185 .unwrap_or(speaker_ip);
1186 (coordinator_id, coord_ip)
1187 }
1188 } else {
1189 (speaker_id.clone(), speaker_ip)
1190 }
1191 }
1192
1193 pub fn event_manager(&self) -> Option<&Arc<SonosEventManager>> {
1198 self.event_manager.get()
1199 }
1200
1201 pub fn set_event_manager(&self, em: Arc<SonosEventManager>) -> Result<()> {
1206 tracing::debug!("StateManager::set_event_manager called");
1207 if self.event_manager.set(Arc::clone(&em)).is_err() {
1208 tracing::debug!("Event manager already set — no-op");
1209 return Ok(()); }
1211
1212 em.set_watch_registry(Arc::new(StateWatchRegistry {
1214 watched: Arc::clone(&self.watched),
1215 ip_to_speaker: Arc::clone(&self.ip_to_speaker),
1216 key_to_service: Arc::clone(&self.key_to_service),
1217 }));
1218
1219 let devices_for_em: Vec<_> = self
1221 .speaker_infos()
1222 .iter()
1223 .map(|info| sonos_discovery::Device {
1224 id: info.id.as_str().to_string(),
1225 name: info.name.clone(),
1226 room_name: info.room_name.clone(),
1227 ip_address: info.ip_address.to_string(),
1228 port: info.port,
1229 model_name: info.model_name.clone(),
1230 })
1231 .collect();
1232
1233 if let Err(e) = em.add_devices(devices_for_em) {
1234 tracing::warn!(
1235 "Failed to add devices to event manager during lazy init: {}",
1236 e
1237 );
1238 }
1239
1240 let worker = spawn_state_event_worker(
1242 em,
1243 Arc::clone(&self.store),
1244 Arc::clone(&self.watched),
1245 Arc::clone(&self.fanout),
1246 Arc::clone(&self.ip_to_speaker),
1247 );
1248 info!("StateManager event worker started (lazy init)");
1249
1250 if let Ok(mut w) = self._worker.lock() {
1251 *w = Some(worker);
1252 }
1253
1254 Ok(())
1255 }
1256
1257 pub fn set_event_init(&self, f: EventInitFn) {
1262 let _ = self.event_init.set(f);
1263 }
1264
1265 pub fn event_init(&self) -> Option<&EventInitFn> {
1270 self.event_init.get()
1271 }
1272}
1273
1274impl Clone for StateManager {
1275 fn clone(&self) -> Self {
1276 let event_manager = OnceLock::new();
1277 if let Some(em) = self.event_manager.get() {
1278 let _ = event_manager.set(Arc::clone(em));
1279 }
1280 let event_init = OnceLock::new();
1281 if let Some(f) = self.event_init.get() {
1282 let _ = event_init.set(Arc::clone(f));
1283 }
1284 Self {
1285 store: Arc::clone(&self.store),
1286 watched: Arc::clone(&self.watched),
1287 ip_to_speaker: Arc::clone(&self.ip_to_speaker),
1288 event_manager,
1289 fanout: Arc::clone(&self.fanout),
1290 _worker: Mutex::new(None),
1291 cleanup_timeout: self.cleanup_timeout,
1292 key_to_service: Arc::clone(&self.key_to_service),
1293 event_init,
1294 }
1295 }
1296}
1297
1298pub struct StateManagerBuilder {
1304 cleanup_timeout: Duration,
1305 event_manager: Option<Arc<SonosEventManager>>,
1306}
1307
1308impl Default for StateManagerBuilder {
1309 fn default() -> Self {
1310 Self {
1311 cleanup_timeout: Duration::from_secs(5),
1312 event_manager: None,
1313 }
1314 }
1315}
1316
1317impl StateManagerBuilder {
1318 pub fn cleanup_timeout(mut self, timeout: Duration) -> Self {
1320 self.cleanup_timeout = timeout;
1321 self
1322 }
1323
1324 pub fn with_event_manager(mut self, em: Arc<SonosEventManager>) -> Self {
1331 self.event_manager = Some(em);
1332 self
1333 }
1334
1335 pub fn build(self) -> Result<StateManager> {
1337 let fanout = Arc::new(EventFanout::new());
1338
1339 let store = Arc::new(RwLock::new(StateStore::new()));
1340 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1341 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
1342 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
1343
1344 let event_manager_lock = OnceLock::new();
1345 let mut worker = None;
1346
1347 if let Some(em) = self.event_manager {
1349 let _ = event_manager_lock.set(Arc::clone(&em));
1350
1351 em.set_watch_registry(Arc::new(StateWatchRegistry {
1353 watched: Arc::clone(&watched),
1354 ip_to_speaker: Arc::clone(&ip_to_speaker),
1355 key_to_service: Arc::clone(&key_to_service),
1356 }));
1357
1358 let worker_handle = spawn_state_event_worker(
1359 em,
1360 Arc::clone(&store),
1361 Arc::clone(&watched),
1362 Arc::clone(&fanout),
1363 Arc::clone(&ip_to_speaker),
1364 );
1365 info!("StateManager event worker started");
1366 worker = Some(worker_handle);
1367 }
1368
1369 let manager = StateManager {
1370 store,
1371 watched,
1372 ip_to_speaker,
1373 event_manager: event_manager_lock,
1374 fanout,
1375 _worker: Mutex::new(worker),
1376 cleanup_timeout: self.cleanup_timeout,
1377 key_to_service,
1378 event_init: OnceLock::new(),
1379 };
1380
1381 info!("StateManager created (sync-first mode)");
1382 Ok(manager)
1383 }
1384}
1385
1386#[cfg(test)]
1387mod tests {
1388 use super::*;
1389 use crate::iter::ChangeIterator;
1390 use crate::property::{GroupVolume, PlaybackState, Volume};
1391 use sonos_api::Service;
1392
1393 fn test_stamp() -> WriteStamp {
1395 WriteStamp::now(ChangeSource::Event)
1396 }
1397
1398 #[test]
1399 fn test_state_manager_creation() {
1400 let manager = StateManager::new().unwrap();
1401 assert!(!manager.is_initialized());
1402 assert_eq!(manager.speaker_count(), 0);
1403 }
1404
1405 #[test]
1406 fn test_add_devices() {
1407 let manager = StateManager::new().unwrap();
1408
1409 let devices = vec![Device {
1410 id: "RINCON_123".to_string(),
1411 name: "Living Room".to_string(),
1412 room_name: "Living Room".to_string(),
1413 ip_address: "192.168.1.100".to_string(),
1414 port: 1400,
1415 model_name: "Sonos One".to_string(),
1416 }];
1417
1418 manager.add_devices(devices).unwrap();
1419 assert_eq!(manager.speaker_count(), 1);
1420 }
1421
1422 #[test]
1423 fn test_property_storage() {
1424 let manager = StateManager::new().unwrap();
1425
1426 let devices = vec![Device {
1427 id: "RINCON_123".to_string(),
1428 name: "Living Room".to_string(),
1429 room_name: "Living Room".to_string(),
1430 ip_address: "192.168.1.100".to_string(),
1431 port: 1400,
1432 model_name: "Sonos One".to_string(),
1433 }];
1434 manager.add_devices(devices).unwrap();
1435
1436 let speaker_id = SpeakerId::new("RINCON_123");
1437
1438 assert!(manager.get_property::<Volume>(&speaker_id).is_none());
1440
1441 manager.set_property(&speaker_id, Volume::new(50));
1443 assert_eq!(
1444 manager.get_property::<Volume>(&speaker_id),
1445 Some(Volume::new(50))
1446 );
1447 }
1448
1449 #[test]
1450 fn test_watch_registration() {
1451 let manager = StateManager::new().unwrap();
1452
1453 let devices = vec![Device {
1454 id: "RINCON_123".to_string(),
1455 name: "Living Room".to_string(),
1456 room_name: "Living Room".to_string(),
1457 ip_address: "192.168.1.100".to_string(),
1458 port: 1400,
1459 model_name: "Sonos One".to_string(),
1460 }];
1461 manager.add_devices(devices).unwrap();
1462
1463 let speaker_id = SpeakerId::new("RINCON_123");
1464
1465 assert!(!manager.is_watched(&speaker_id, "volume"));
1467
1468 manager.register_watch(&speaker_id, "volume");
1470 assert!(manager.is_watched(&speaker_id, "volume"));
1471
1472 manager.unregister_watch(&speaker_id, "volume");
1474 assert!(!manager.is_watched(&speaker_id, "volume"));
1475 }
1476
1477 #[test]
1478 fn test_change_event_emission() {
1479 let manager = StateManager::new().unwrap();
1480
1481 let devices = vec![Device {
1482 id: "RINCON_123".to_string(),
1483 name: "Living Room".to_string(),
1484 room_name: "Living Room".to_string(),
1485 ip_address: "192.168.1.100".to_string(),
1486 port: 1400,
1487 model_name: "Sonos One".to_string(),
1488 }];
1489 manager.add_devices(devices).unwrap();
1490
1491 let speaker_id = SpeakerId::new("RINCON_123");
1492
1493 manager.register_watch(&speaker_id, "volume");
1495
1496 let iter = manager.iter();
1499
1500 manager.set_property(&speaker_id, Volume::new(75));
1502 let event = iter.recv_timeout(std::time::Duration::from_millis(100));
1503 assert!(event.is_some());
1504
1505 let event = event.unwrap();
1506 assert_eq!(event.speaker_id.as_str(), "RINCON_123");
1507 assert_eq!(event.property_key(), "volume");
1508 }
1509
1510 #[test]
1511 fn test_set_group_property_emits_change_event() {
1512 let manager = StateManager::new().unwrap();
1513
1514 let devices = vec![Device {
1515 id: "RINCON_123".to_string(),
1516 name: "Living Room".to_string(),
1517 room_name: "Living Room".to_string(),
1518 ip_address: "192.168.1.100".to_string(),
1519 port: 1400,
1520 model_name: "Sonos One".to_string(),
1521 }];
1522 manager.add_devices(devices).unwrap();
1523
1524 let speaker_id = SpeakerId::new("RINCON_123");
1525 let group_id = GroupId::new("RINCON_123:1");
1526
1527 {
1529 let mut store = manager.store.write();
1530 store.add_group(GroupInfo::new(
1531 group_id.clone(),
1532 speaker_id.clone(),
1533 vec![speaker_id.clone()],
1534 ));
1535 }
1536
1537 manager.register_watch(&speaker_id, "group_volume");
1539
1540 let iter = manager.iter();
1541
1542 manager.set_group_property(&group_id, GroupVolume::new(80));
1544
1545 let event = iter.recv_timeout(std::time::Duration::from_millis(100));
1547 assert!(event.is_some());
1548
1549 let event = event.unwrap();
1550 assert_eq!(event.speaker_id.as_str(), "RINCON_123");
1551 assert_eq!(event.property_key(), "group_volume");
1552 assert_eq!(event.service(), Service::GroupRenderingControl);
1553 }
1554
1555 #[test]
1556 fn test_set_group_property_no_event_when_unwatched() {
1557 let manager = StateManager::new().unwrap();
1558
1559 let devices = vec![Device {
1560 id: "RINCON_123".to_string(),
1561 name: "Living Room".to_string(),
1562 room_name: "Living Room".to_string(),
1563 ip_address: "192.168.1.100".to_string(),
1564 port: 1400,
1565 model_name: "Sonos One".to_string(),
1566 }];
1567 manager.add_devices(devices).unwrap();
1568
1569 let speaker_id = SpeakerId::new("RINCON_123");
1570 let group_id = GroupId::new("RINCON_123:1");
1571
1572 {
1573 let mut store = manager.store.write();
1574 store.add_group(GroupInfo::new(
1575 group_id.clone(),
1576 speaker_id.clone(),
1577 vec![speaker_id.clone()],
1578 ));
1579 }
1580
1581 let iter = manager.iter();
1582
1583 manager.set_group_property(&group_id, GroupVolume::new(50));
1585 let event = iter.recv_timeout(std::time::Duration::from_millis(100));
1586 assert!(event.is_none());
1587 }
1588
1589 #[test]
1594 fn test_add_group_updates_speaker_to_group() {
1595 let mut store = StateStore::new();
1596
1597 let speaker1 = SpeakerId::new("RINCON_111");
1598 let speaker2 = SpeakerId::new("RINCON_222");
1599 let group_id = GroupId::new("RINCON_111:1");
1600
1601 let group = GroupInfo::new(
1602 group_id.clone(),
1603 speaker1.clone(),
1604 vec![speaker1.clone(), speaker2.clone()],
1605 );
1606
1607 store.add_group(group);
1608
1609 assert_eq!(store.speaker_to_group.get(&speaker1), Some(&group_id));
1611 assert_eq!(store.speaker_to_group.get(&speaker2), Some(&group_id));
1612 }
1613
1614 #[test]
1615 fn test_add_group_single_speaker() {
1616 let mut store = StateStore::new();
1617
1618 let speaker = SpeakerId::new("RINCON_333");
1619 let group_id = GroupId::new("RINCON_333:1");
1620
1621 let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
1622
1623 store.add_group(group.clone());
1624
1625 assert_eq!(store.speaker_to_group.get(&speaker), Some(&group_id));
1627
1628 assert_eq!(store.groups.get(&group_id), Some(&group));
1630 }
1631
1632 #[test]
1633 fn test_get_group_for_speaker_returns_correct_group() {
1634 let mut store = StateStore::new();
1635
1636 let speaker1 = SpeakerId::new("RINCON_111");
1637 let speaker2 = SpeakerId::new("RINCON_222");
1638 let speaker3 = SpeakerId::new("RINCON_333");
1639 let group1_id = GroupId::new("RINCON_111:1");
1640 let group2_id = GroupId::new("RINCON_333:1");
1641
1642 let group1 = GroupInfo::new(
1644 group1_id.clone(),
1645 speaker1.clone(),
1646 vec![speaker1.clone(), speaker2.clone()],
1647 );
1648
1649 let group2 = GroupInfo::new(group2_id.clone(), speaker3.clone(), vec![speaker3.clone()]);
1651
1652 store.add_group(group1.clone());
1653 store.add_group(group2.clone());
1654
1655 assert_eq!(store.get_group_for_speaker(&speaker1), Some(&group1));
1657 assert_eq!(store.get_group_for_speaker(&speaker2), Some(&group1));
1658 assert_eq!(store.get_group_for_speaker(&speaker3), Some(&group2));
1659 }
1660
1661 #[test]
1662 fn test_get_group_for_speaker_returns_none_for_unknown() {
1663 let store = StateStore::new();
1664
1665 let unknown_speaker = SpeakerId::new("RINCON_UNKNOWN");
1666
1667 assert!(store.get_group_for_speaker(&unknown_speaker).is_none());
1668 }
1669
1670 #[test]
1671 fn test_clear_groups_removes_all_group_data() {
1672 let mut store = StateStore::new();
1673
1674 let speaker1 = SpeakerId::new("RINCON_111");
1675 let speaker2 = SpeakerId::new("RINCON_222");
1676 let group_id = GroupId::new("RINCON_111:1");
1677
1678 let group = GroupInfo::new(
1679 group_id.clone(),
1680 speaker1.clone(),
1681 vec![speaker1.clone(), speaker2.clone()],
1682 );
1683
1684 store.add_group(group);
1685
1686 assert!(!store.groups.is_empty());
1688 assert!(!store.speaker_to_group.is_empty());
1689
1690 store.clear_groups();
1692
1693 assert!(store.groups.is_empty());
1695 assert!(store.group_props.is_empty());
1696 assert!(store.speaker_to_group.is_empty());
1697 }
1698
1699 #[test]
1700 fn test_clear_groups_then_add_new_groups() {
1701 let mut store = StateStore::new();
1702
1703 let speaker1 = SpeakerId::new("RINCON_111");
1705 let group1_id = GroupId::new("RINCON_111:1");
1706 let group1 = GroupInfo::new(group1_id.clone(), speaker1.clone(), vec![speaker1.clone()]);
1707 store.add_group(group1);
1708
1709 store.clear_groups();
1711
1712 let speaker2 = SpeakerId::new("RINCON_222");
1713 let group2_id = GroupId::new("RINCON_222:1");
1714 let group2 = GroupInfo::new(group2_id.clone(), speaker2.clone(), vec![speaker2.clone()]);
1715 store.add_group(group2.clone());
1716
1717 assert!(!store.groups.contains_key(&group1_id));
1719 assert_eq!(store.groups.get(&group2_id), Some(&group2));
1720
1721 assert!(!store.speaker_to_group.contains_key(&speaker1));
1723 assert_eq!(store.speaker_to_group.get(&speaker2), Some(&group2_id));
1724 }
1725
1726 #[test]
1731 fn test_state_manager_groups_returns_all_groups() {
1732 let manager = StateManager::new().unwrap();
1733
1734 let devices = vec![
1736 Device {
1737 id: "RINCON_111".to_string(),
1738 name: "Living Room".to_string(),
1739 room_name: "Living Room".to_string(),
1740 ip_address: "192.168.1.100".to_string(),
1741 port: 1400,
1742 model_name: "Sonos One".to_string(),
1743 },
1744 Device {
1745 id: "RINCON_222".to_string(),
1746 name: "Kitchen".to_string(),
1747 room_name: "Kitchen".to_string(),
1748 ip_address: "192.168.1.101".to_string(),
1749 port: 1400,
1750 model_name: "Sonos One".to_string(),
1751 },
1752 ];
1753 manager.add_devices(devices).unwrap();
1754
1755 let speaker1 = SpeakerId::new("RINCON_111");
1757 let speaker2 = SpeakerId::new("RINCON_222");
1758 let group1 = GroupInfo::new(
1759 GroupId::new("RINCON_111:1"),
1760 speaker1.clone(),
1761 vec![speaker1.clone()],
1762 );
1763 let group2 = GroupInfo::new(
1764 GroupId::new("RINCON_222:1"),
1765 speaker2.clone(),
1766 vec![speaker2.clone()],
1767 );
1768
1769 let topology = Topology::new(
1770 manager.speaker_infos(),
1771 vec![group1.clone(), group2.clone()],
1772 );
1773 manager.initialize(topology);
1774
1775 let groups = manager.groups();
1777 assert_eq!(groups.len(), 2);
1778
1779 let group_ids: Vec<_> = groups.iter().map(|g| g.id.clone()).collect();
1781 assert!(group_ids.contains(&GroupId::new("RINCON_111:1")));
1782 assert!(group_ids.contains(&GroupId::new("RINCON_222:1")));
1783 }
1784
1785 #[test]
1786 fn test_state_manager_groups_returns_empty_when_no_groups() {
1787 let manager = StateManager::new().unwrap();
1788
1789 let groups = manager.groups();
1791 assert!(groups.is_empty());
1792 }
1793
1794 #[test]
1795 fn test_state_manager_get_group_returns_correct_group() {
1796 let manager = StateManager::new().unwrap();
1797
1798 let devices = vec![Device {
1800 id: "RINCON_111".to_string(),
1801 name: "Living Room".to_string(),
1802 room_name: "Living Room".to_string(),
1803 ip_address: "192.168.1.100".to_string(),
1804 port: 1400,
1805 model_name: "Sonos One".to_string(),
1806 }];
1807 manager.add_devices(devices).unwrap();
1808
1809 let speaker = SpeakerId::new("RINCON_111");
1811 let group_id = GroupId::new("RINCON_111:1");
1812 let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
1813
1814 let topology = Topology::new(manager.speaker_infos(), vec![group.clone()]);
1815 manager.initialize(topology);
1816
1817 let found = manager.get_group(&group_id);
1819 assert!(found.is_some());
1820 assert_eq!(found.unwrap(), group);
1821 }
1822
1823 #[test]
1824 fn test_state_manager_get_group_returns_none_for_unknown() {
1825 let manager = StateManager::new().unwrap();
1826
1827 let unknown_id = GroupId::new("RINCON_UNKNOWN:1");
1829 let found = manager.get_group(&unknown_id);
1830 assert!(found.is_none());
1831 }
1832
1833 #[test]
1834 fn test_state_manager_get_group_for_speaker_returns_correct_group() {
1835 let manager = StateManager::new().unwrap();
1836
1837 let devices = vec![
1839 Device {
1840 id: "RINCON_111".to_string(),
1841 name: "Living Room".to_string(),
1842 room_name: "Living Room".to_string(),
1843 ip_address: "192.168.1.100".to_string(),
1844 port: 1400,
1845 model_name: "Sonos One".to_string(),
1846 },
1847 Device {
1848 id: "RINCON_222".to_string(),
1849 name: "Kitchen".to_string(),
1850 room_name: "Kitchen".to_string(),
1851 ip_address: "192.168.1.101".to_string(),
1852 port: 1400,
1853 model_name: "Sonos One".to_string(),
1854 },
1855 ];
1856 manager.add_devices(devices).unwrap();
1857
1858 let speaker1 = SpeakerId::new("RINCON_111");
1860 let speaker2 = SpeakerId::new("RINCON_222");
1861 let group_id = GroupId::new("RINCON_111:1");
1862 let group = GroupInfo::new(
1863 group_id.clone(),
1864 speaker1.clone(),
1865 vec![speaker1.clone(), speaker2.clone()],
1866 );
1867
1868 let topology = Topology::new(manager.speaker_infos(), vec![group.clone()]);
1869 manager.initialize(topology);
1870
1871 let found1 = manager.get_group_for_speaker(&speaker1);
1873 assert!(found1.is_some());
1874 assert_eq!(found1.unwrap(), group);
1875
1876 let found2 = manager.get_group_for_speaker(&speaker2);
1877 assert!(found2.is_some());
1878 assert_eq!(found2.unwrap(), group);
1879 }
1880
1881 #[test]
1882 fn test_state_manager_get_group_for_speaker_returns_none_for_unknown() {
1883 let manager = StateManager::new().unwrap();
1884
1885 let unknown_speaker = SpeakerId::new("RINCON_UNKNOWN");
1887 let found = manager.get_group_for_speaker(&unknown_speaker);
1888 assert!(found.is_none());
1889 }
1890
1891 #[test]
1892 fn test_state_manager_group_methods_consistency() {
1893 let manager = StateManager::new().unwrap();
1894
1895 let devices = vec![Device {
1897 id: "RINCON_111".to_string(),
1898 name: "Living Room".to_string(),
1899 room_name: "Living Room".to_string(),
1900 ip_address: "192.168.1.100".to_string(),
1901 port: 1400,
1902 model_name: "Sonos One".to_string(),
1903 }];
1904 manager.add_devices(devices).unwrap();
1905
1906 let speaker = SpeakerId::new("RINCON_111");
1908 let group_id = GroupId::new("RINCON_111:1");
1909 let group = GroupInfo::new(group_id.clone(), speaker.clone(), vec![speaker.clone()]);
1910
1911 let topology = Topology::new(manager.speaker_infos(), vec![group.clone()]);
1912 manager.initialize(topology);
1913
1914 let groups = manager.groups();
1916 assert_eq!(groups.len(), 1);
1917 assert_eq!(groups[0], group);
1918
1919 let by_id = manager.get_group(&group_id);
1920 assert_eq!(by_id, Some(group.clone()));
1921
1922 let by_speaker = manager.get_group_for_speaker(&speaker);
1923 assert_eq!(by_speaker, Some(group.clone()));
1924
1925 assert_eq!(groups[0], by_id.unwrap());
1927 assert_eq!(groups[0], by_speaker.unwrap());
1928 }
1929
1930 #[test]
1935 fn test_get_boot_seq_returns_none_for_unknown_speaker() {
1936 let manager = StateManager::new().unwrap();
1937 let unknown = SpeakerId::new("RINCON_UNKNOWN");
1938 assert!(manager.get_boot_seq(&unknown).is_none());
1939 }
1940
1941 #[test]
1942 fn test_boot_seq_defaults_to_zero_for_new_speaker() {
1943 let manager = StateManager::new().unwrap();
1944
1945 let devices = vec![Device {
1946 id: "RINCON_123".to_string(),
1947 name: "Living Room".to_string(),
1948 room_name: "Living Room".to_string(),
1949 ip_address: "192.168.1.100".to_string(),
1950 port: 1400,
1951 model_name: "Sonos One".to_string(),
1952 }];
1953 manager.add_devices(devices).unwrap();
1954
1955 let speaker_id = SpeakerId::new("RINCON_123");
1956
1957 assert_eq!(manager.get_boot_seq(&speaker_id), Some(0));
1959 }
1960
1961 #[test]
1966 fn test_state_watch_registry_register_and_unregister() {
1967 let watched = Arc::new(RwLock::new(WatchCounts::new()));
1968 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
1969 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
1970
1971 let ip: IpAddr = "192.168.1.100".parse().unwrap();
1972 let speaker_id = SpeakerId::new("RINCON_123");
1973 ip_to_speaker.write().insert(ip, speaker_id.clone());
1974
1975 let registry = StateWatchRegistry {
1976 watched: Arc::clone(&watched),
1977 ip_to_speaker: Arc::clone(&ip_to_speaker),
1978 key_to_service: Arc::clone(&key_to_service),
1979 };
1980
1981 registry.register_watch(&speaker_id, "volume", Service::RenderingControl);
1983 registry.register_watch(&speaker_id, "mute", Service::RenderingControl);
1984 registry.register_watch(&speaker_id, "playback_state", Service::AVTransport);
1985
1986 assert_eq!(watched.read().len(), 3);
1987
1988 registry.unregister_watches_for_service(ip, Service::RenderingControl);
1990
1991 let w = watched.read();
1992 assert_eq!(w.len(), 1);
1993 assert!(is_pair_watched(&w, &speaker_id, "playback_state"));
1994 assert!(!is_pair_watched(&w, &speaker_id, "volume"));
1995 assert!(!is_pair_watched(&w, &speaker_id, "mute"));
1996 }
1997
1998 #[test]
1999 fn test_state_watch_registry_unknown_ip_is_noop() {
2000 let watched = Arc::new(RwLock::new(WatchCounts::new()));
2001 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
2002 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
2003
2004 let speaker_id = SpeakerId::new("RINCON_123");
2005
2006 let registry = StateWatchRegistry {
2007 watched: Arc::clone(&watched),
2008 ip_to_speaker,
2009 key_to_service: Arc::clone(&key_to_service),
2010 };
2011
2012 retain_direct_watch(&watched, &speaker_id, "volume");
2014 key_to_service
2015 .write()
2016 .insert("volume", Service::RenderingControl);
2017
2018 let unknown_ip: IpAddr = "10.0.0.1".parse().unwrap();
2020 registry.unregister_watches_for_service(unknown_ip, Service::RenderingControl);
2021
2022 assert_eq!(watched.read().len(), 1);
2024 }
2025
2026 #[test]
2027 fn test_state_watch_registry_only_removes_matching_speaker() {
2028 let watched = Arc::new(RwLock::new(WatchCounts::new()));
2029 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
2030 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
2031
2032 let ip1: IpAddr = "192.168.1.100".parse().unwrap();
2033 let ip2: IpAddr = "192.168.1.101".parse().unwrap();
2034 let speaker1 = SpeakerId::new("RINCON_111");
2035 let speaker2 = SpeakerId::new("RINCON_222");
2036
2037 ip_to_speaker.write().insert(ip1, speaker1.clone());
2038 ip_to_speaker.write().insert(ip2, speaker2.clone());
2039
2040 let registry = StateWatchRegistry {
2041 watched: Arc::clone(&watched),
2042 ip_to_speaker,
2043 key_to_service: Arc::clone(&key_to_service),
2044 };
2045
2046 registry.register_watch(&speaker1, "volume", Service::RenderingControl);
2048 registry.register_watch(&speaker2, "volume", Service::RenderingControl);
2049 assert_eq!(watched.read().len(), 2);
2050
2051 registry.unregister_watches_for_service(ip1, Service::RenderingControl);
2053
2054 let w = watched.read();
2055 assert_eq!(w.len(), 1);
2056 assert!(is_pair_watched(&w, &speaker2, "volume"));
2057 assert!(!is_pair_watched(&w, &speaker1, "volume"));
2058 }
2059
2060 #[test]
2071 fn test_watch_refcount_survives_partial_release() {
2072 let manager = StateManager::new().unwrap();
2073 let speaker_id = SpeakerId::new("RINCON_123");
2074
2075 manager.register_watch(&speaker_id, "volume");
2076 manager.register_watch(&speaker_id, "volume");
2077 assert!(manager.is_watched(&speaker_id, "volume"));
2078
2079 manager.unregister_watch(&speaker_id, "volume");
2081 assert!(
2082 manager.is_watched(&speaker_id, "volume"),
2083 "one of two watchers released — the property must stay watched"
2084 );
2085
2086 manager.unregister_watch(&speaker_id, "volume");
2088 assert!(!manager.is_watched(&speaker_id, "volume"));
2089
2090 manager.unregister_watch(&speaker_id, "volume");
2092 assert!(!manager.is_watched(&speaker_id, "volume"));
2093 }
2094
2095 #[test]
2103 fn test_service_unregister_keeps_directly_held_watches() {
2104 let watched = Arc::new(RwLock::new(WatchCounts::new()));
2105 let ip_to_speaker = Arc::new(RwLock::new(HashMap::new()));
2106 let key_to_service = Arc::new(RwLock::new(HashMap::new()));
2107
2108 let ip: IpAddr = "192.168.1.100".parse().unwrap();
2109 let speaker_id = SpeakerId::new("RINCON_123");
2110 ip_to_speaker.write().insert(ip, speaker_id.clone());
2111
2112 let registry = StateWatchRegistry {
2113 watched: Arc::clone(&watched),
2114 ip_to_speaker,
2115 key_to_service,
2116 };
2117
2118 registry.register_watch(&speaker_id, "volume", Service::RenderingControl);
2120 retain_direct_watch(&watched, &speaker_id, "volume");
2121 registry.register_watch(&speaker_id, "mute", Service::RenderingControl);
2123 retain_direct_watch(&watched, &speaker_id, "mute");
2124
2125 registry.unregister_watches_for_service(ip, Service::RenderingControl);
2126
2127 let w = watched.read();
2128 assert!(
2129 is_pair_watched(&w, &speaker_id, "volume"),
2130 "the direct hold on volume must survive the subscription teardown"
2131 );
2132 assert!(
2133 is_pair_watched(&w, &speaker_id, "mute"),
2134 "the direct hold on mute must survive the subscription teardown"
2135 );
2136 }
2137
2138 #[test]
2150 fn test_set_property_on_group_member_is_readable_from_both() {
2151 let manager = StateManager::new().unwrap();
2152
2153 let devices = vec![
2154 Device {
2155 id: "RINCON_COORD".to_string(),
2156 name: "Living Room".to_string(),
2157 room_name: "Living Room".to_string(),
2158 ip_address: "192.168.1.100".to_string(),
2159 port: 1400,
2160 model_name: "Sonos One".to_string(),
2161 },
2162 Device {
2163 id: "RINCON_MEMBER".to_string(),
2164 name: "Kitchen".to_string(),
2165 room_name: "Kitchen".to_string(),
2166 ip_address: "192.168.1.101".to_string(),
2167 port: 1400,
2168 model_name: "Sonos One".to_string(),
2169 },
2170 ];
2171 manager.add_devices(devices).unwrap();
2172
2173 let coordinator = SpeakerId::new("RINCON_COORD");
2174 let member = SpeakerId::new("RINCON_MEMBER");
2175 let group_id = GroupId::new("RINCON_COORD:1");
2176 let topology = Topology::new(
2177 manager.speaker_infos(),
2178 vec![GroupInfo::new(
2179 group_id,
2180 coordinator.clone(),
2181 vec![coordinator.clone(), member.clone()],
2182 )],
2183 );
2184 manager.initialize(topology);
2185
2186 manager.set_property(&member, PlaybackState::Playing);
2189
2190 assert_eq!(
2191 manager.get_property::<PlaybackState>(&member),
2192 Some(PlaybackState::Playing),
2193 "the member must be able to read back what it just wrote"
2194 );
2195 assert_eq!(
2196 manager.get_property::<PlaybackState>(&coordinator),
2197 Some(PlaybackState::Playing),
2198 "the write belongs in the coordinator's bag, which is where reads resolve"
2199 );
2200
2201 manager.set_property(&member, Volume::new(33));
2203 assert_eq!(
2204 manager.get_property::<Volume>(&member),
2205 Some(Volume::new(33))
2206 );
2207 assert_eq!(
2208 manager.get_property::<Volume>(&coordinator),
2209 None,
2210 "PerSpeaker writes must not be redirected to the coordinator"
2211 );
2212 }
2213
2214 #[test]
2219 fn test_resolve_coordinator_for_standalone_speaker() {
2220 let mut store = StateStore::new();
2221
2222 let speaker = SpeakerId::new("RINCON_111");
2223 let group_id = GroupId::new("RINCON_111:1");
2224
2225 store.add_speaker(SpeakerInfo {
2226 id: speaker.clone(),
2227 name: "Living Room".to_string(),
2228 room_name: "Living Room".to_string(),
2229 ip_address: "192.168.1.100".parse().unwrap(),
2230 port: 1400,
2231 model_name: "Test".to_string(),
2232 software_version: "1.0".to_string(),
2233 boot_seq: 0,
2234 satellites: vec![],
2235 });
2236 store.add_group(GroupInfo::new(
2237 group_id,
2238 speaker.clone(),
2239 vec![speaker.clone()],
2240 ));
2241
2242 assert_eq!(store.resolve_coordinator(&speaker), speaker);
2244 }
2245
2246 #[test]
2247 fn test_resolve_coordinator_for_group_member() {
2248 let mut store = StateStore::new();
2249
2250 let coordinator = SpeakerId::new("RINCON_COORD");
2251 let member = SpeakerId::new("RINCON_MEMBER");
2252 let group_id = GroupId::new("RINCON_COORD:1");
2253
2254 store.add_group(GroupInfo::new(
2255 group_id,
2256 coordinator.clone(),
2257 vec![coordinator.clone(), member.clone()],
2258 ));
2259
2260 assert_eq!(store.resolve_coordinator(&member), coordinator);
2262 assert_eq!(store.resolve_coordinator(&coordinator), coordinator);
2264 }
2265
2266 #[test]
2267 fn test_resolve_coordinator_no_group_data() {
2268 let store = StateStore::new();
2269
2270 let speaker = SpeakerId::new("RINCON_UNKNOWN");
2271
2272 assert_eq!(store.resolve_coordinator(&speaker), speaker);
2274 }
2275
2276 #[test]
2281 fn test_get_resolved_per_coordinator_reads_from_coordinator() {
2282 let mut store = StateStore::new();
2283
2284 let coordinator = SpeakerId::new("RINCON_COORD");
2285 let member = SpeakerId::new("RINCON_MEMBER");
2286 let group_id = GroupId::new("RINCON_COORD:1");
2287
2288 store.add_speaker(SpeakerInfo {
2289 id: coordinator.clone(),
2290 name: "Coord".to_string(),
2291 room_name: "Coord".to_string(),
2292 ip_address: "192.168.1.100".parse().unwrap(),
2293 port: 1400,
2294 model_name: "Test".to_string(),
2295 software_version: "1.0".to_string(),
2296 boot_seq: 0,
2297 satellites: vec![],
2298 });
2299 store.add_speaker(SpeakerInfo {
2300 id: member.clone(),
2301 name: "Member".to_string(),
2302 room_name: "Member".to_string(),
2303 ip_address: "192.168.1.101".parse().unwrap(),
2304 port: 1400,
2305 model_name: "Test".to_string(),
2306 software_version: "1.0".to_string(),
2307 boot_seq: 0,
2308 satellites: vec![],
2309 });
2310 store.add_group(GroupInfo::new(
2311 group_id,
2312 coordinator.clone(),
2313 vec![coordinator.clone(), member.clone()],
2314 ));
2315
2316 store.set(&coordinator, PlaybackState::Playing, test_stamp());
2318
2319 let resolved: Option<PlaybackState> = store.get_resolved(&member);
2321 assert_eq!(resolved, Some(PlaybackState::Playing));
2322
2323 let direct: Option<PlaybackState> = store.get(&member);
2325 assert_eq!(direct, None);
2326 }
2327
2328 #[test]
2329 fn test_get_resolved_per_speaker_reads_own_props() {
2330 let mut store = StateStore::new();
2331
2332 let coordinator = SpeakerId::new("RINCON_COORD");
2333 let member = SpeakerId::new("RINCON_MEMBER");
2334 let group_id = GroupId::new("RINCON_COORD:1");
2335
2336 store.add_speaker(SpeakerInfo {
2337 id: coordinator.clone(),
2338 name: "Coord".to_string(),
2339 room_name: "Coord".to_string(),
2340 ip_address: "192.168.1.100".parse().unwrap(),
2341 port: 1400,
2342 model_name: "Test".to_string(),
2343 software_version: "1.0".to_string(),
2344 boot_seq: 0,
2345 satellites: vec![],
2346 });
2347 store.add_speaker(SpeakerInfo {
2348 id: member.clone(),
2349 name: "Member".to_string(),
2350 room_name: "Member".to_string(),
2351 ip_address: "192.168.1.101".parse().unwrap(),
2352 port: 1400,
2353 model_name: "Test".to_string(),
2354 software_version: "1.0".to_string(),
2355 boot_seq: 0,
2356 satellites: vec![],
2357 });
2358 store.add_group(GroupInfo::new(
2359 group_id,
2360 coordinator.clone(),
2361 vec![coordinator.clone(), member.clone()],
2362 ));
2363
2364 store.set(&coordinator, Volume::new(80), test_stamp());
2366
2367 let resolved: Option<Volume> = store.get_resolved(&member);
2369 assert_eq!(resolved, None);
2370
2371 let coord_resolved: Option<Volume> = store.get_resolved(&coordinator);
2373 assert_eq!(coord_resolved, Some(Volume::new(80)));
2374 }
2375
2376 #[test]
2377 fn test_update_speaker_ip() {
2378 let manager = StateManager::new().unwrap();
2379
2380 let devices = vec![Device {
2381 id: "RINCON_111".to_string(),
2382 name: "Office".to_string(),
2383 room_name: "Office".to_string(),
2384 ip_address: "192.168.4.198".to_string(),
2385 port: 1400,
2386 model_name: "Roam 2".to_string(),
2387 }];
2388 manager.add_devices(devices).unwrap();
2389
2390 let speaker_id = SpeakerId::new("RINCON_111");
2391 let old_ip: IpAddr = "192.168.4.198".parse().unwrap();
2392 let new_ip: IpAddr = "192.168.4.200".parse().unwrap();
2393
2394 assert_eq!(manager.get_speaker_ip(&speaker_id), Some(old_ip));
2396
2397 manager.update_speaker_ip(&speaker_id, new_ip);
2399
2400 assert_eq!(manager.get_speaker_ip(&speaker_id), Some(new_ip));
2402
2403 let ip_map = manager.ip_to_speaker.read();
2405 assert!(!ip_map.contains_key(&old_ip));
2406 assert_eq!(ip_map.get(&new_ip), Some(&speaker_id));
2407 }
2408
2409 #[test]
2410 fn test_update_speaker_ip_no_change() {
2411 let manager = StateManager::new().unwrap();
2412
2413 let devices = vec![Device {
2414 id: "RINCON_111".to_string(),
2415 name: "Office".to_string(),
2416 room_name: "Office".to_string(),
2417 ip_address: "192.168.4.198".to_string(),
2418 port: 1400,
2419 model_name: "Roam 2".to_string(),
2420 }];
2421 manager.add_devices(devices).unwrap();
2422
2423 let speaker_id = SpeakerId::new("RINCON_111");
2424 let same_ip: IpAddr = "192.168.4.198".parse().unwrap();
2425
2426 manager.update_speaker_ip(&speaker_id, same_ip);
2428 assert_eq!(manager.get_speaker_ip(&speaker_id), Some(same_ip));
2429 }
2430
2431 #[test]
2432 fn test_satellite_ids() {
2433 let manager = StateManager::new().unwrap();
2434
2435 assert!(manager.get_satellite_ids().is_empty());
2436
2437 let ids = vec![SpeakerId::new("RINCON_SAT1"), SpeakerId::new("RINCON_SAT2")];
2438 manager.set_satellite_ids(ids.clone());
2439
2440 let stored = manager.get_satellite_ids();
2441 assert_eq!(stored.len(), 2);
2442 assert!(stored.contains(&SpeakerId::new("RINCON_SAT1")));
2443 assert!(stored.contains(&SpeakerId::new("RINCON_SAT2")));
2444 }
2445
2446 #[test]
2462 fn test_queued_events_preserve_every_intermediate_value() {
2463 let manager = StateManager::new().unwrap();
2464 manager
2465 .add_devices(vec![Device {
2466 id: "RINCON_QUEUE".to_string(),
2467 name: "Queue Test".to_string(),
2468 room_name: "Test".to_string(),
2469 ip_address: "192.0.2.10".to_string(),
2470 port: 1400,
2471 model_name: "Sonos One".to_string(),
2472 }])
2473 .unwrap();
2474
2475 let speaker_id = SpeakerId::new("RINCON_QUEUE");
2476 manager.register_watch(&speaker_id, PlaybackState::KEY);
2477
2478 let iter = manager.iter();
2479
2480 manager.set_property(&speaker_id, PlaybackState::Playing);
2483 manager.set_property(&speaker_id, PlaybackState::Transitioning);
2484 manager.set_property(&speaker_id, PlaybackState::Playing);
2485 let observed: Vec<PlaybackState> = iter
2486 .try_iter()
2487 .filter_map(|e| match e.change {
2488 PropertyChange::PlaybackState(s) => Some(s),
2489 _ => None,
2490 })
2491 .collect();
2492
2493 assert_eq!(
2494 observed,
2495 vec![
2496 PlaybackState::Playing,
2497 PlaybackState::Transitioning,
2498 PlaybackState::Playing,
2499 ],
2500 "every queued value must be observable from the event stream"
2501 );
2502
2503 assert_eq!(
2506 manager.get_property::<PlaybackState>(&speaker_id),
2507 Some(PlaybackState::Playing)
2508 );
2509 }
2510
2511 fn manager_watching_volume() -> (StateManager, SpeakerId) {
2513 let manager = StateManager::new().unwrap();
2514 manager
2515 .add_devices(vec![Device {
2516 id: "RINCON_FANOUT".to_string(),
2517 name: "Living Room".to_string(),
2518 room_name: "Living Room".to_string(),
2519 ip_address: "192.0.2.10".to_string(),
2521 port: 1400,
2522 model_name: "Sonos One".to_string(),
2523 }])
2524 .unwrap();
2525 let speaker_id = SpeakerId::new("RINCON_FANOUT");
2526 manager.register_watch(&speaker_id, Volume::KEY);
2527 (manager, speaker_id)
2528 }
2529
2530 fn volumes_from(iter: &ChangeIterator) -> Vec<u8> {
2531 iter.try_iter()
2532 .filter_map(|e| match e.change {
2533 PropertyChange::Volume(v) => Some(v.value()),
2534 _ => None,
2535 })
2536 .collect()
2537 }
2538
2539 #[test]
2547 fn test_two_iterators_each_receive_every_event() {
2548 let (manager, speaker_id) = manager_watching_volume();
2549
2550 let dashboard = manager.iter();
2551 let logger = manager.iter();
2552
2553 for v in [10u8, 20, 30, 40] {
2554 manager.set_property(&speaker_id, Volume::new(v));
2555 }
2556
2557 assert_eq!(
2559 volumes_from(&dashboard),
2560 vec![10, 20, 30, 40],
2561 "the first iterator must see every event"
2562 );
2563 assert_eq!(
2564 volumes_from(&logger),
2565 vec![10, 20, 30, 40],
2566 "the second iterator must see every event too, not a subset"
2567 );
2568 }
2569
2570 #[test]
2574 fn test_single_iterator_receives_every_event_in_order() {
2575 let (manager, speaker_id) = manager_watching_volume();
2576
2577 let iter = manager.iter();
2578 let sent: Vec<u8> = (1..=25).collect();
2579 for &v in &sent {
2580 manager.set_property(&speaker_id, Volume::new(v));
2581 }
2582
2583 assert_eq!(volumes_from(&iter), sent);
2584 }
2585
2586 #[test]
2592 fn test_dropped_consumer_does_not_stall_survivor() {
2593 let (manager, speaker_id) = manager_watching_volume();
2594
2595 let survivor = manager.iter();
2596 let departing = manager.iter();
2597
2598 manager.set_property(&speaker_id, Volume::new(5));
2599 drop(departing);
2600
2601 manager.set_property(&speaker_id, Volume::new(6));
2603 manager.set_property(&speaker_id, Volume::new(7));
2604
2605 assert_eq!(
2606 volumes_from(&survivor),
2607 vec![5, 6, 7],
2608 "the remaining consumer must keep receiving after a sibling drops"
2609 );
2610
2611 let latecomer = manager.iter();
2613 manager.set_property(&speaker_id, Volume::new(8));
2614 assert_eq!(volumes_from(&latecomer), vec![8]);
2615 }
2616
2617 #[test]
2623 fn test_stale_fetch_does_not_clobber_newer_event_value() {
2624 let manager = StateManager::new().unwrap();
2625 manager
2626 .add_devices(vec![Device {
2627 id: "RINCON_RACE".to_string(),
2628 name: "Race Test".to_string(),
2629 room_name: "Test".to_string(),
2630 ip_address: "192.0.2.11".to_string(),
2631 port: 1400,
2632 model_name: "Sonos One".to_string(),
2633 }])
2634 .unwrap();
2635
2636 let speaker_id = SpeakerId::new("RINCON_RACE");
2637
2638 let fetch_observed_at = Instant::now();
2641
2642 let event_outcome = manager.set_property_stamped(
2644 &speaker_id,
2645 Volume::new(40),
2646 WriteStamp::now(ChangeSource::Event),
2647 );
2648 assert_eq!(event_outcome, WriteOutcome::Changed);
2649
2650 let fetch_outcome = manager.set_property_stamped(
2652 &speaker_id,
2653 Volume::new(10),
2654 WriteStamp::observed_at(ChangeSource::Fetch, fetch_observed_at),
2655 );
2656
2657 assert_eq!(
2658 fetch_outcome,
2659 WriteOutcome::Stale,
2660 "a fetch observed before the stored event must be rejected"
2661 );
2662 assert_eq!(
2663 manager.get_property::<Volume>(&speaker_id),
2664 Some(Volume::new(40)),
2665 "the newer event value must survive the late fetch response"
2666 );
2667 }
2668
2669 #[test]
2673 fn test_newer_write_is_accepted_after_an_earlier_one() {
2674 let manager = StateManager::new().unwrap();
2675 manager
2676 .add_devices(vec![Device {
2677 id: "RINCON_FWD".to_string(),
2678 name: "Forward Test".to_string(),
2679 room_name: "Test".to_string(),
2680 ip_address: "192.0.2.12".to_string(),
2681 port: 1400,
2682 model_name: "Sonos One".to_string(),
2683 }])
2684 .unwrap();
2685
2686 let speaker_id = SpeakerId::new("RINCON_FWD");
2687 let early = Instant::now();
2688
2689 assert_eq!(
2690 manager.set_property_stamped(
2691 &speaker_id,
2692 Volume::new(10),
2693 WriteStamp::observed_at(ChangeSource::Fetch, early),
2694 ),
2695 WriteOutcome::Changed
2696 );
2697
2698 assert_eq!(
2700 manager.set_property_stamped(
2701 &speaker_id,
2702 Volume::new(20),
2703 WriteStamp::now(ChangeSource::Fetch),
2704 ),
2705 WriteOutcome::Changed
2706 );
2707 assert_eq!(
2708 manager.get_property::<Volume>(&speaker_id),
2709 Some(Volume::new(20))
2710 );
2711 }
2712}