1use std::fmt;
9use std::marker::PhantomData;
10use std::net::IpAddr;
11use std::sync::Arc;
12use std::time::Instant;
13
14use sonos_api::operation::{ComposableOperation, UPnPOperation};
15use sonos_api::{ServiceScope, SonosClient};
16use sonos_event_manager::WatchGuard;
17use sonos_state::{property::SonosProperty, ChangeSource, SpeakerId, StateManager, WriteStamp};
18
19use crate::SdkError;
20
21#[derive(Clone)]
26pub struct SpeakerContext {
27 pub(crate) speaker_id: SpeakerId,
28 pub(crate) speaker_ip: IpAddr,
29 pub(crate) state_manager: Arc<StateManager>,
30 pub(crate) api_client: SonosClient,
31}
32
33impl SpeakerContext {
34 pub fn new(
36 speaker_id: SpeakerId,
37 speaker_ip: IpAddr,
38 state_manager: Arc<StateManager>,
39 api_client: SonosClient,
40 ) -> Arc<Self> {
41 Arc::new(Self {
42 speaker_id,
43 speaker_ip,
44 state_manager,
45 api_client,
46 })
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub enum WatchMode {
60 Events,
65
66 Polling,
72
73 CacheOnly,
78}
79
80impl fmt::Display for WatchMode {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 match self {
83 WatchMode::Events => write!(f, "Events (real-time)"),
84 WatchMode::Polling => write!(f, "Polling (fallback)"),
85 WatchMode::CacheOnly => write!(f, "CacheOnly (no events)"),
86 }
87 }
88}
89
90#[must_use = "dropping the handle starts the grace period — hold it to keep the subscription alive"]
130pub struct WatchHandle<P> {
131 read: Box<dyn Fn() -> Option<P> + Send + Sync>,
139 mode: WatchMode,
140 _cleanup: WatchCleanup,
141}
142
143impl<P> WatchHandle<P> {
144 pub fn mode(&self) -> WatchMode {
146 self.mode
147 }
148
149 pub fn value(&self) -> Option<P> {
159 (self.read)()
160 }
161
162 pub fn has_value(&self) -> bool {
167 self.value().is_some()
168 }
169
170 pub fn has_realtime_events(&self) -> bool {
172 self.mode == WatchMode::Events
173 }
174}
175
176impl<P: fmt::Debug> fmt::Debug for WatchHandle<P> {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.debug_struct("WatchHandle")
179 .field("value", &self.value())
180 .field("mode", &self.mode)
181 .finish()
182 }
183}
184
185#[allow(dead_code)]
196enum WatchCleanup {
197 Guard(WatchGuard),
198 CacheOnly(CacheOnlyGuard),
199 CoordinatorGuard {
200 _guard: WatchGuard,
201 _member_cleanup: CacheOnlyGuard,
202 },
203}
204
205struct CacheOnlyGuard {
213 state_manager: Arc<StateManager>,
214 speaker_id: SpeakerId,
215 property_key: &'static str,
216}
217
218impl Drop for CacheOnlyGuard {
219 fn drop(&mut self) {
220 self.state_manager
223 .unregister_watch(&self.speaker_id, self.property_key);
224 }
225}
226
227pub trait Fetchable: SonosProperty {
254 type Operation: UPnPOperation;
256
257 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
259
260 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
262}
263
264pub trait FetchableWithContext: SonosProperty {
269 type Operation: UPnPOperation;
271
272 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
274
275 fn from_response_with_context(
277 response: <Self::Operation as UPnPOperation>::Response,
278 speaker_id: &SpeakerId,
279 ) -> Option<Self>;
280}
281
282#[derive(Clone)]
307pub struct PropertyHandle<P: SonosProperty> {
308 context: Arc<SpeakerContext>,
309 _phantom: PhantomData<P>,
310}
311
312impl<P: SonosProperty> PropertyHandle<P> {
313 pub fn new(context: Arc<SpeakerContext>) -> Self {
315 Self {
316 context,
317 _phantom: PhantomData,
318 }
319 }
320
321 #[must_use = "returns the cached property value"]
334 pub fn get(&self) -> Option<P> {
335 self.context
336 .state_manager
337 .get_property::<P>(&self.context.speaker_id)
338 }
339
340 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
367 tracing::trace!(
368 "watch() called for {:?} on {}",
369 P::SERVICE,
370 self.context.speaker_id.as_str()
371 );
372
373 if self.context.state_manager.event_manager().is_none() {
375 if let Some(init) = self.context.state_manager.event_init() {
376 tracing::debug!(
377 "Event manager not initialized, triggering lazy init for {:?} on {}",
378 P::SERVICE,
379 self.context.speaker_id.as_str()
380 );
381 init().map_err(|e| SdkError::EventManager(e.to_string()))?;
382 } else {
383 tracing::debug!(
384 "No event_init closure available (test mode?) for {}",
385 self.context.speaker_id.as_str()
386 );
387 }
388 }
389
390 let (sub_id, sub_ip) = self.context.state_manager.resolve_subscription_target(
392 &self.context.speaker_id,
393 self.context.speaker_ip,
394 P::SERVICE,
395 );
396 let routed_to_coordinator = sub_id != self.context.speaker_id;
397
398 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
399 match em.acquire_watch(&sub_id, P::KEY, sub_ip, P::SERVICE) {
400 Ok(guard) => {
401 if routed_to_coordinator {
402 self.context
404 .state_manager
405 .register_watch(&self.context.speaker_id, P::KEY);
406 (
407 WatchMode::Events,
408 WatchCleanup::CoordinatorGuard {
409 _guard: guard,
410 _member_cleanup: CacheOnlyGuard {
411 state_manager: Arc::clone(&self.context.state_manager),
412 speaker_id: self.context.speaker_id.clone(),
413 property_key: P::KEY,
414 },
415 },
416 )
417 } else {
418 (WatchMode::Events, WatchCleanup::Guard(guard))
419 }
420 }
421 Err(e) => {
422 tracing::warn!(
423 "Failed to subscribe to {:?} for {}: {} - falling back to polling",
424 P::SERVICE,
425 self.context.speaker_id.as_str(),
426 e
427 );
428 self.context
430 .state_manager
431 .register_watch(&self.context.speaker_id, P::KEY);
432 (
433 WatchMode::Polling,
434 WatchCleanup::CacheOnly(CacheOnlyGuard {
435 state_manager: Arc::clone(&self.context.state_manager),
436 speaker_id: self.context.speaker_id.clone(),
437 property_key: P::KEY,
438 }),
439 )
440 }
441 }
442 } else {
443 tracing::warn!(
445 "No event manager available for {} — falling back to cache-only mode",
446 self.context.speaker_id.as_str()
447 );
448 self.context
449 .state_manager
450 .register_watch(&self.context.speaker_id, P::KEY);
451 (
452 WatchMode::CacheOnly,
453 WatchCleanup::CacheOnly(CacheOnlyGuard {
454 state_manager: Arc::clone(&self.context.state_manager),
455 speaker_id: self.context.speaker_id.clone(),
456 property_key: P::KEY,
457 }),
458 )
459 };
460
461 tracing::debug!(
462 "watch() resolved to {:?} for {} on {}",
463 mode,
464 P::KEY,
465 self.context.speaker_id.as_str()
466 );
467
468 let context = Arc::clone(&self.context);
475 Ok(WatchHandle {
476 read: Box::new(move || context.state_manager.get_property::<P>(&context.speaker_id)),
477 mode,
478 _cleanup: cleanup,
479 })
480 }
481
482 #[must_use = "returns whether the property is being watched"]
498 pub fn is_watched(&self) -> bool {
499 self.context
500 .state_manager
501 .is_watched(&self.context.speaker_id, P::KEY)
502 }
503
504 pub fn speaker_id(&self) -> &SpeakerId {
506 &self.context.speaker_id
507 }
508
509 pub fn speaker_ip(&self) -> IpAddr {
511 self.context.speaker_ip
512 }
513}
514
515impl<P: Fetchable> PropertyHandle<P> {
520 pub fn watch_or_fetch(&self) -> Result<WatchHandle<P>, SdkError> {
526 let wh = self.watch()?;
527 if !wh.has_value() {
528 if let Err(e) = self.fetch() {
534 tracing::warn!("watch_or_fetch: fetch failed for {}: {e}", P::KEY);
535 }
536 }
537 Ok(wh)
538 }
539
540 #[must_use = "returns the fetched value from the device"]
556 pub fn fetch(&self) -> Result<P, SdkError> {
557 let operation = P::build_operation()?;
558
559 let (target_id, target_ip) = if P::SERVICE.scope() == ServiceScope::PerCoordinator {
561 self.context.state_manager.resolve_subscription_target(
562 &self.context.speaker_id,
563 self.context.speaker_ip,
564 P::SERVICE,
565 )
566 } else {
567 let current_ip = self
568 .context
569 .state_manager
570 .get_speaker_ip(&self.context.speaker_id)
571 .unwrap_or(self.context.speaker_ip);
572 (self.context.speaker_id.clone(), current_ip)
573 };
574
575 let observed_at = Instant::now();
581
582 let response = self
583 .context
584 .api_client
585 .execute_enhanced(&target_ip.to_string(), operation)
586 .map_err(SdkError::ApiError)?;
587
588 let property_value = P::from_response(response);
589
590 self.context.state_manager.set_property_stamped(
594 &target_id,
595 property_value.clone(),
596 WriteStamp::observed_at(ChangeSource::Fetch, observed_at),
597 );
598
599 Ok(property_value)
600 }
601}
602
603impl PropertyHandle<GroupMembership> {
612 #[must_use = "returns the fetched value from the device"]
617 pub fn fetch(&self) -> Result<GroupMembership, SdkError> {
618 let operation = <GroupMembership as FetchableWithContext>::build_operation()?;
619
620 let observed_at = Instant::now();
622
623 let response = self
624 .context
625 .api_client
626 .execute_enhanced(&self.context.speaker_ip.to_string(), operation)
627 .map_err(SdkError::ApiError)?;
628
629 let property_value =
630 GroupMembership::from_response_with_context(response, &self.context.speaker_id)
631 .ok_or_else(|| {
632 SdkError::FetchFailed(format!(
633 "Speaker {} not found in topology response",
634 self.context.speaker_id.as_str()
635 ))
636 })?;
637
638 self.context.state_manager.set_property_stamped(
639 &self.context.speaker_id,
640 property_value.clone(),
641 WriteStamp::observed_at(ChangeSource::Fetch, observed_at),
642 );
643
644 Ok(property_value)
645 }
646}
647
648use sonos_api::services::{
653 av_transport::{
654 self, GetPositionInfoOperation, GetPositionInfoResponse, GetTransportInfoOperation,
655 GetTransportInfoResponse,
656 },
657 group_rendering_control::{
658 self, GetGroupMuteOperation, GetGroupMuteResponse, GetGroupVolumeOperation,
659 GetGroupVolumeResponse,
660 },
661 rendering_control::{
662 self, GetBassOperation, GetBassResponse, GetLoudnessOperation, GetLoudnessResponse,
663 GetMuteOperation, GetMuteResponse, GetTrebleOperation, GetTrebleResponse,
664 GetVolumeOperation, GetVolumeResponse,
665 },
666 zone_group_topology::{self, GetZoneGroupStateOperation, GetZoneGroupStateResponse},
667};
668use sonos_state::{
669 Bass, CurrentTrack, GroupId, GroupMembership, GroupMute, GroupVolume, GroupVolumeChangeable,
670 Loudness, Mute, PlaybackState, Position, Treble, Volume,
671};
672
673fn build_error<E: std::fmt::Display>(operation_name: &str, e: E) -> SdkError {
679 SdkError::FetchFailed(format!("Failed to build {operation_name} operation: {e}"))
680}
681
682impl Fetchable for Volume {
687 type Operation = GetVolumeOperation;
688
689 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
690 rendering_control::get_volume_operation("Master".to_string())
691 .build()
692 .map_err(|e| build_error("GetVolume", e))
693 }
694
695 fn from_response(response: GetVolumeResponse) -> Self {
696 Volume::new(response.current_volume)
697 }
698}
699
700impl Fetchable for PlaybackState {
701 type Operation = GetTransportInfoOperation;
702
703 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
704 av_transport::get_transport_info_operation()
705 .build()
706 .map_err(|e| build_error("GetTransportInfo", e))
707 }
708
709 fn from_response(response: GetTransportInfoResponse) -> Self {
710 match response.current_transport_state.as_str() {
711 "PLAYING" => PlaybackState::Playing,
712 "PAUSED" | "PAUSED_PLAYBACK" => PlaybackState::Paused,
713 "STOPPED" => PlaybackState::Stopped,
714 _ => PlaybackState::Transitioning,
715 }
716 }
717}
718
719impl Fetchable for Position {
720 type Operation = GetPositionInfoOperation;
721
722 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
723 av_transport::get_position_info_operation()
724 .build()
725 .map_err(|e| build_error("GetPositionInfo", e))
726 }
727
728 fn from_response(response: GetPositionInfoResponse) -> Self {
729 let position_ms = Position::parse_time_to_ms(&response.rel_time).unwrap_or(0);
730 let duration_ms = Position::parse_time_to_ms(&response.track_duration).unwrap_or(0);
731 Position::new(position_ms, duration_ms)
732 }
733}
734
735impl Fetchable for Mute {
736 type Operation = GetMuteOperation;
737
738 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
739 rendering_control::get_mute_operation("Master".to_string())
740 .build()
741 .map_err(|e| build_error("GetMute", e))
742 }
743
744 fn from_response(response: GetMuteResponse) -> Self {
745 Mute::new(response.current_mute)
746 }
747}
748
749impl Fetchable for Bass {
750 type Operation = GetBassOperation;
751
752 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
753 rendering_control::get_bass_operation()
754 .build()
755 .map_err(|e| build_error("GetBass", e))
756 }
757
758 fn from_response(response: GetBassResponse) -> Self {
759 Bass::new(response.current_bass)
760 }
761}
762
763impl Fetchable for Treble {
764 type Operation = GetTrebleOperation;
765
766 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
767 rendering_control::get_treble_operation()
768 .build()
769 .map_err(|e| build_error("GetTreble", e))
770 }
771
772 fn from_response(response: GetTrebleResponse) -> Self {
773 Treble::new(response.current_treble)
774 }
775}
776
777impl Fetchable for Loudness {
778 type Operation = GetLoudnessOperation;
779
780 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
781 rendering_control::get_loudness_operation("Master".to_string())
782 .build()
783 .map_err(|e| build_error("GetLoudness", e))
784 }
785
786 fn from_response(response: GetLoudnessResponse) -> Self {
787 Loudness::new(response.current_loudness)
788 }
789}
790
791impl Fetchable for CurrentTrack {
792 type Operation = GetPositionInfoOperation;
793
794 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
795 av_transport::get_position_info_operation()
796 .build()
797 .map_err(|e| build_error("GetPositionInfo", e))
798 }
799
800 fn from_response(response: GetPositionInfoResponse) -> Self {
801 let metadata = if response.track_meta_data.is_empty()
802 || response.track_meta_data == "NOT_IMPLEMENTED"
803 {
804 None
805 } else {
806 Some(response.track_meta_data.as_str())
807 };
808 let (title, artist, album, album_art_uri) = sonos_state::parse_track_metadata(metadata);
809 CurrentTrack {
810 title,
811 artist,
812 album,
813 album_art_uri,
814 uri: Some(response.track_uri).filter(|s| !s.is_empty()),
815 }
816 }
817}
818
819impl FetchableWithContext for GroupMembership {
824 type Operation = GetZoneGroupStateOperation;
825
826 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
827 zone_group_topology::get_zone_group_state_operation()
828 .build()
829 .map_err(|e| build_error("GetZoneGroupState", e))
830 }
831
832 fn from_response_with_context(
833 response: GetZoneGroupStateResponse,
834 speaker_id: &SpeakerId,
835 ) -> Option<Self> {
836 let zone_groups =
837 zone_group_topology::parse_zone_group_state_xml(&response.zone_group_state).ok()?;
838
839 for group in &zone_groups {
840 let is_member = group.members.iter().any(|m| m.uuid == speaker_id.as_str());
841 if is_member {
842 let is_coordinator = group.coordinator == speaker_id.as_str();
843 return Some(GroupMembership::new(
844 GroupId::new(&group.id),
845 is_coordinator,
846 ));
847 }
848 }
849
850 None
851 }
852}
853
854pub type VolumeHandle = PropertyHandle<Volume>;
871
872pub type PlaybackStateHandle = PropertyHandle<PlaybackState>;
874
875pub type MuteHandle = PropertyHandle<Mute>;
877
878pub type BassHandle = PropertyHandle<Bass>;
880
881pub type TrebleHandle = PropertyHandle<Treble>;
883
884pub type LoudnessHandle = PropertyHandle<Loudness>;
886
887pub type PositionHandle = PropertyHandle<Position>;
889
890pub type CurrentTrackHandle = PropertyHandle<CurrentTrack>;
892
893pub type GroupMembershipHandle = PropertyHandle<GroupMembership>;
895
896#[derive(Clone)]
905pub struct GroupContext {
906 pub(crate) group_id: GroupId,
907 pub(crate) coordinator_id: SpeakerId,
908 pub(crate) coordinator_ip: IpAddr,
909 pub(crate) state_manager: Arc<StateManager>,
910 pub(crate) api_client: SonosClient,
911}
912
913impl GroupContext {
914 pub fn new(
916 group_id: GroupId,
917 coordinator_id: SpeakerId,
918 coordinator_ip: IpAddr,
919 state_manager: Arc<StateManager>,
920 api_client: SonosClient,
921 ) -> Arc<Self> {
922 Arc::new(Self {
923 group_id,
924 coordinator_id,
925 coordinator_ip,
926 state_manager,
927 api_client,
928 })
929 }
930}
931
932#[derive(Clone)]
938pub struct GroupPropertyHandle<P: SonosProperty> {
939 context: Arc<GroupContext>,
940 _phantom: PhantomData<P>,
941}
942
943impl<P: SonosProperty> GroupPropertyHandle<P> {
944 pub fn new(context: Arc<GroupContext>) -> Self {
946 Self {
947 context,
948 _phantom: PhantomData,
949 }
950 }
951
952 #[must_use = "returns the cached property value"]
954 pub fn get(&self) -> Option<P> {
955 self.context
956 .state_manager
957 .get_group_property::<P>(&self.context.group_id)
958 }
959
960 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
965 if self.context.state_manager.event_manager().is_none() {
967 if let Some(init) = self.context.state_manager.event_init() {
968 tracing::debug!(
969 "Event manager not initialized, triggering lazy init for group {:?} on {}",
970 P::SERVICE,
971 self.context.group_id.as_str()
972 );
973 init().map_err(|e| SdkError::EventManager(e.to_string()))?;
974 } else {
975 tracing::debug!(
976 "No event_init closure available (test mode?) for group {}",
977 self.context.group_id.as_str()
978 );
979 }
980 }
981
982 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
983 match em.acquire_watch(
984 &self.context.coordinator_id,
985 P::KEY,
986 self.context.coordinator_ip,
987 P::SERVICE,
988 ) {
989 Ok(guard) => (WatchMode::Events, WatchCleanup::Guard(guard)),
990 Err(e) => {
991 tracing::warn!(
992 "Failed to subscribe to {:?} for group {}: {} - falling back to polling",
993 P::SERVICE,
994 self.context.group_id.as_str(),
995 e
996 );
997 self.context
998 .state_manager
999 .register_watch(&self.context.coordinator_id, P::KEY);
1000 (
1001 WatchMode::Polling,
1002 WatchCleanup::CacheOnly(CacheOnlyGuard {
1003 state_manager: Arc::clone(&self.context.state_manager),
1004 speaker_id: self.context.coordinator_id.clone(),
1005 property_key: P::KEY,
1006 }),
1007 )
1008 }
1009 }
1010 } else {
1011 self.context
1012 .state_manager
1013 .register_watch(&self.context.coordinator_id, P::KEY);
1014 (
1015 WatchMode::CacheOnly,
1016 WatchCleanup::CacheOnly(CacheOnlyGuard {
1017 state_manager: Arc::clone(&self.context.state_manager),
1018 speaker_id: self.context.coordinator_id.clone(),
1019 property_key: P::KEY,
1020 }),
1021 )
1022 };
1023
1024 let context = Arc::clone(&self.context);
1026 Ok(WatchHandle {
1027 read: Box::new(move || {
1028 context
1029 .state_manager
1030 .get_group_property::<P>(&context.group_id)
1031 }),
1032 mode,
1033 _cleanup: cleanup,
1034 })
1035 }
1036
1037 #[must_use = "returns whether the property is being watched"]
1039 pub fn is_watched(&self) -> bool {
1040 self.context
1041 .state_manager
1042 .is_watched(&self.context.coordinator_id, P::KEY)
1043 }
1044
1045 pub fn group_id(&self) -> &GroupId {
1047 &self.context.group_id
1048 }
1049}
1050
1051pub trait GroupFetchable: SonosProperty {
1053 type Operation: UPnPOperation;
1055
1056 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
1058
1059 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
1061}
1062
1063impl<P: GroupFetchable> GroupPropertyHandle<P> {
1064 pub fn watch_or_fetch(&self) -> Result<WatchHandle<P>, SdkError> {
1067 let wh = self.watch()?;
1068 if !wh.has_value() {
1069 if let Err(e) = self.fetch() {
1072 tracing::warn!(
1073 "watch_or_fetch: fetch failed for group {} {}: {e}",
1074 self.context.group_id.as_str(),
1075 P::KEY
1076 );
1077 }
1078 }
1079 Ok(wh)
1080 }
1081
1082 #[must_use = "returns the fetched value from the device"]
1084 pub fn fetch(&self) -> Result<P, SdkError> {
1085 let operation = P::build_operation()?;
1086
1087 let observed_at = Instant::now();
1089
1090 let response = self
1091 .context
1092 .api_client
1093 .execute_enhanced(&self.context.coordinator_ip.to_string(), operation)
1094 .map_err(SdkError::ApiError)?;
1095
1096 let property_value = P::from_response(response);
1097
1098 self.context.state_manager.set_group_property_stamped(
1099 &self.context.group_id,
1100 property_value.clone(),
1101 WriteStamp::observed_at(ChangeSource::Fetch, observed_at),
1102 );
1103
1104 Ok(property_value)
1105 }
1106}
1107
1108impl GroupFetchable for GroupVolume {
1113 type Operation = GetGroupVolumeOperation;
1114
1115 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
1116 group_rendering_control::get_group_volume()
1117 .build()
1118 .map_err(|e| build_error("GetGroupVolume", e))
1119 }
1120
1121 fn from_response(response: GetGroupVolumeResponse) -> Self {
1122 GroupVolume::new(response.current_volume)
1123 }
1124}
1125
1126impl GroupFetchable for GroupMute {
1127 type Operation = GetGroupMuteOperation;
1128
1129 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
1130 group_rendering_control::get_group_mute()
1131 .build()
1132 .map_err(|e| build_error("GetGroupMute", e))
1133 }
1134
1135 fn from_response(response: GetGroupMuteResponse) -> Self {
1136 GroupMute::new(response.current_mute)
1137 }
1138}
1139
1140pub type GroupVolumeHandle = GroupPropertyHandle<GroupVolume>;
1146
1147pub type GroupMuteHandle = GroupPropertyHandle<GroupMute>;
1149
1150pub type GroupVolumeChangeableHandle = GroupPropertyHandle<GroupVolumeChangeable>;
1152
1153#[cfg(test)]
1154mod tests {
1155 use super::*;
1156 use sonos_discovery::Device;
1157 use sonos_state::Property;
1158
1159 fn create_test_state_manager() -> Arc<StateManager> {
1160 let manager = StateManager::new().unwrap();
1161 let devices = vec![Device {
1162 id: "RINCON_TEST123".to_string(),
1163 name: "Test Speaker".to_string(),
1164 room_name: "Test Room".to_string(),
1165 ip_address: "192.168.1.100".to_string(),
1166 port: 1400,
1167 model_name: "Sonos One".to_string(),
1168 }];
1169 manager.add_devices(devices).unwrap();
1170 Arc::new(manager)
1171 }
1172
1173 fn create_test_context(state_manager: Arc<StateManager>) -> Arc<SpeakerContext> {
1174 SpeakerContext::new(
1175 SpeakerId::new("RINCON_TEST123"),
1176 "192.168.1.100".parse().unwrap(),
1177 state_manager,
1178 SonosClient::new(),
1179 )
1180 }
1181
1182 #[test]
1183 fn test_property_handle_creation() {
1184 let state_manager = create_test_state_manager();
1185 let context = create_test_context(state_manager);
1186 let speaker_ip: IpAddr = "192.168.1.100".parse().unwrap();
1187
1188 let handle: VolumeHandle = PropertyHandle::new(context);
1189
1190 assert_eq!(handle.speaker_id().as_str(), "RINCON_TEST123");
1191 assert_eq!(handle.speaker_ip(), speaker_ip);
1192 }
1193
1194 #[test]
1195 fn test_get_returns_none_initially() {
1196 let state_manager = create_test_state_manager();
1197 let context = create_test_context(state_manager);
1198
1199 let handle: VolumeHandle = PropertyHandle::new(context);
1200
1201 assert!(handle.get().is_none());
1202 }
1203
1204 #[test]
1205 fn test_get_returns_cached_value() {
1206 let state_manager = create_test_state_manager();
1207 let speaker_id = SpeakerId::new("RINCON_TEST123");
1208
1209 state_manager.set_property(&speaker_id, Volume::new(75));
1210
1211 let context = create_test_context(Arc::clone(&state_manager));
1212 let handle: VolumeHandle = PropertyHandle::new(context);
1213
1214 assert_eq!(handle.get(), Some(Volume::new(75)));
1215 }
1216
1217 #[test]
1218 fn test_watch_registers_property() {
1219 let state_manager = create_test_state_manager();
1220 let context = create_test_context(Arc::clone(&state_manager));
1221
1222 let handle: VolumeHandle = PropertyHandle::new(context);
1223
1224 assert!(!handle.is_watched());
1225 let _wh = handle.watch().unwrap();
1226 assert!(handle.is_watched());
1227 }
1228
1229 #[test]
1230 fn test_drop_watch_handle_unregisters_property() {
1231 let state_manager = create_test_state_manager();
1232 let context = create_test_context(Arc::clone(&state_manager));
1233
1234 let handle: VolumeHandle = PropertyHandle::new(context);
1235
1236 let wh = handle.watch().unwrap();
1237 assert!(handle.is_watched());
1238
1239 drop(wh);
1240 assert!(!handle.is_watched());
1241 }
1242
1243 #[test]
1259 fn test_dropping_one_of_two_handles_keeps_property_emitting() {
1260 let state_manager = create_test_state_manager();
1261 let speaker_id = SpeakerId::new("RINCON_TEST123");
1262 let context = create_test_context(Arc::clone(&state_manager));
1263
1264 let volume: VolumeHandle = PropertyHandle::new(Arc::clone(&context));
1265 let mute: MuteHandle = PropertyHandle::new(context);
1266
1267 let first = volume.watch().unwrap();
1268 let second = volume.watch().unwrap();
1269 let _mute_watch = mute.watch().unwrap();
1271 assert!(volume.is_watched());
1272 assert!(mute.is_watched());
1273
1274 drop(first);
1275
1276 assert!(
1277 volume.is_watched(),
1278 "one of two Volume handles dropped — the property must stay watched"
1279 );
1280 assert!(
1281 mute.is_watched(),
1282 "releasing a Volume handle must not disturb its RenderingControl sibling"
1283 );
1284
1285 let iter = state_manager.iter();
1289 state_manager.set_property(&speaker_id, Volume::new(11));
1290 state_manager.set_property(&speaker_id, Mute::new(true));
1291
1292 let first_event = iter
1293 .recv_timeout(std::time::Duration::from_millis(100))
1294 .expect("Volume is still held by `second` and must still emit");
1295 assert_eq!(first_event.property_key(), Volume::KEY);
1296 let second_event = iter
1297 .recv_timeout(std::time::Duration::from_millis(100))
1298 .expect("Mute is still held and must still emit");
1299 assert_eq!(second_event.property_key(), Mute::KEY);
1300
1301 drop(second);
1303 assert!(!volume.is_watched());
1304 state_manager.set_property(&speaker_id, Volume::new(22));
1305 assert!(
1306 iter.recv_timeout(std::time::Duration::from_millis(50))
1307 .is_none(),
1308 "with every Volume handle dropped the property must stop emitting"
1309 );
1310 }
1311
1312 #[test]
1318 fn test_sdk_change_event_carries_value_and_source() {
1319 let state_manager = create_test_state_manager();
1320 let speaker_id = SpeakerId::new("RINCON_TEST123");
1321
1322 let context = create_test_context(Arc::clone(&state_manager));
1323 let handle: VolumeHandle = PropertyHandle::new(context);
1324 let _wh = handle.watch().unwrap();
1325
1326 let iter = state_manager.iter();
1327 state_manager.set_property(&speaker_id, Volume::new(37));
1328
1329 let event = iter
1330 .recv_timeout(std::time::Duration::from_millis(100))
1331 .expect("a watched property write must emit");
1332
1333 assert!(
1334 matches!(
1335 event.change,
1336 sonos_state::PropertyChange::Volume(Volume(37))
1337 ),
1338 "the event must carry the written value, got {:?}",
1339 event.change
1340 );
1341 assert_eq!(
1342 event.source,
1343 ChangeSource::LocalAction,
1344 "`set_property` is a local write, not a device report"
1345 );
1346 }
1347
1348 #[test]
1362 fn test_handle_held_across_change_reports_new_value() {
1363 let state_manager = create_test_state_manager();
1364 let speaker_id = SpeakerId::new("RINCON_TEST123");
1365
1366 state_manager.set_property(&speaker_id, Volume::new(10));
1367
1368 let context = create_test_context(Arc::clone(&state_manager));
1369 let handle: VolumeHandle = PropertyHandle::new(context);
1370
1371 let wh = handle.watch().unwrap();
1373 assert_eq!(wh.value(), Some(Volume::new(10)));
1374
1375 state_manager.set_property(&speaker_id, Volume::new(42));
1376 assert_eq!(
1377 wh.value(),
1378 Some(Volume::new(42)),
1379 "the handle froze its value at creation — a held handle must read live"
1380 );
1381
1382 state_manager.set_property(&speaker_id, Volume::new(43));
1383 assert_eq!(
1384 wh.value(),
1385 Some(Volume::new(43)),
1386 "the handle must keep tracking, not refresh once"
1387 );
1388 }
1389
1390 #[test]
1398 fn test_handle_acquired_before_first_value_becomes_populated() {
1399 let state_manager = create_test_state_manager();
1400 let speaker_id = SpeakerId::new("RINCON_TEST123");
1401
1402 let context = create_test_context(Arc::clone(&state_manager));
1403 let handle: VolumeHandle = PropertyHandle::new(context);
1404
1405 let wh = handle.watch().unwrap();
1406 assert!(!wh.has_value(), "nothing has been observed yet");
1407 assert_eq!(wh.value(), None);
1408
1409 state_manager.set_property(&speaker_id, Volume::new(7));
1410
1411 assert!(
1412 wh.has_value(),
1413 "has_value() froze at creation — it must reflect the store"
1414 );
1415 assert_eq!(wh.value(), Some(Volume::new(7)));
1416 }
1417
1418 #[test]
1426 fn test_all_handles_see_update_and_survive_a_sibling_drop() {
1427 let state_manager = create_test_state_manager();
1428 let speaker_id = SpeakerId::new("RINCON_TEST123");
1429 let context = create_test_context(Arc::clone(&state_manager));
1430
1431 let handle: VolumeHandle = PropertyHandle::new(Arc::clone(&context));
1432
1433 let first = handle.watch().unwrap();
1434 let second = handle.watch().unwrap();
1435 let third = handle.watch().unwrap();
1436
1437 let iter = state_manager.iter();
1438 state_manager.set_property(&speaker_id, Volume::new(31));
1439
1440 assert_eq!(first.value(), Some(Volume::new(31)));
1442 assert_eq!(second.value(), Some(Volume::new(31)));
1443 assert_eq!(third.value(), Some(Volume::new(31)));
1444 assert!(iter
1445 .recv_timeout(std::time::Duration::from_millis(100))
1446 .is_some());
1447
1448 drop(first);
1449 assert!(handle.is_watched(), "two handles still hold the property");
1450
1451 state_manager.set_property(&speaker_id, Volume::new(32));
1453 assert_eq!(
1454 second.value(),
1455 Some(Volume::new(32)),
1456 "a sibling handle dropping must not freeze the survivors"
1457 );
1458 assert_eq!(third.value(), Some(Volume::new(32)));
1459 assert!(
1460 iter.recv_timeout(std::time::Duration::from_millis(100))
1461 .is_some(),
1462 "the property is still held and must still emit"
1463 );
1464
1465 drop(second);
1466 state_manager.set_property(&speaker_id, Volume::new(33));
1467 assert_eq!(
1468 third.value(),
1469 Some(Volume::new(33)),
1470 "the last handle must still read live"
1471 );
1472 }
1473
1474 #[test]
1488 fn test_handle_reads_none_when_value_becomes_unreachable() {
1489 let manager = StateManager::new().unwrap();
1490 manager
1491 .add_devices(vec![
1492 Device {
1493 id: "RINCON_MEMBER".to_string(),
1494 name: "Member".to_string(),
1495 room_name: "Member".to_string(),
1496 ip_address: "203.0.113.1".to_string(),
1497 port: 1400,
1498 model_name: "Sonos One".to_string(),
1499 },
1500 Device {
1501 id: "RINCON_NEWCOORD".to_string(),
1502 name: "New Coordinator".to_string(),
1503 room_name: "New Coordinator".to_string(),
1504 ip_address: "203.0.113.2".to_string(),
1505 port: 1400,
1506 model_name: "Sonos One".to_string(),
1507 },
1508 ])
1509 .unwrap();
1510 let state_manager = Arc::new(manager);
1511
1512 let member = SpeakerId::new("RINCON_MEMBER");
1513 let new_coord = SpeakerId::new("RINCON_NEWCOORD");
1514
1515 state_manager.set_property(&member, PlaybackState::Playing);
1517
1518 let context = SpeakerContext::new(
1519 member.clone(),
1520 "203.0.113.1".parse().unwrap(),
1521 Arc::clone(&state_manager),
1522 SonosClient::new(),
1523 );
1524 let handle: PlaybackStateHandle = PropertyHandle::new(context);
1525 let wh = handle.watch().unwrap();
1526 assert_eq!(wh.value(), Some(PlaybackState::Playing));
1527
1528 state_manager.initialize(sonos_state::Topology {
1530 speakers: vec![],
1531 groups: vec![sonos_state::GroupInfo::new(
1532 GroupId::new("RINCON_NEWCOORD:1"),
1533 new_coord.clone(),
1534 vec![new_coord.clone(), member.clone()],
1535 )],
1536 });
1537
1538 assert_eq!(
1539 wh.value(),
1540 None,
1541 "the coordinator holds no PlaybackState, so the answer is unknown — \
1542 a handle must not report the value it captured at creation"
1543 );
1544
1545 state_manager.set_property(&new_coord, PlaybackState::Paused);
1547 assert_eq!(wh.value(), Some(PlaybackState::Paused));
1548 }
1549
1550 #[test]
1551 fn test_watch_returns_current_value() {
1552 let state_manager = create_test_state_manager();
1553 let speaker_id = SpeakerId::new("RINCON_TEST123");
1554
1555 state_manager.set_property(&speaker_id, Volume::new(50));
1556
1557 let context = create_test_context(Arc::clone(&state_manager));
1558 let handle: VolumeHandle = PropertyHandle::new(context);
1559
1560 let wh = handle.watch().unwrap();
1561 assert_eq!(wh.value(), Some(Volume::new(50)));
1562 assert_eq!(wh.mode(), WatchMode::CacheOnly);
1564 }
1565
1566 #[test]
1567 fn test_watch_handle_accessors() {
1568 let state_manager = create_test_state_manager();
1569 let speaker_id = SpeakerId::new("RINCON_TEST123");
1570
1571 state_manager.set_property(&speaker_id, Volume::new(75));
1572
1573 let context = create_test_context(Arc::clone(&state_manager));
1574 let handle: VolumeHandle = PropertyHandle::new(context);
1575
1576 let wh = handle.watch().unwrap();
1577 assert!(wh.has_value());
1578 assert!(!wh.has_realtime_events());
1579 assert_eq!(wh.value().map(|v| v.value()), Some(75));
1580 }
1581
1582 #[test]
1583 fn test_property_handle_clone() {
1584 let state_manager = create_test_state_manager();
1585 let speaker_id = SpeakerId::new("RINCON_TEST123");
1586
1587 state_manager.set_property(&speaker_id, Volume::new(60));
1588
1589 let context = create_test_context(Arc::clone(&state_manager));
1590 let handle: VolumeHandle = PropertyHandle::new(context);
1591
1592 let cloned = handle.clone();
1593
1594 assert_eq!(handle.get(), cloned.get());
1595 assert_eq!(handle.get(), Some(Volume::new(60)));
1596 }
1597
1598 fn create_test_group_context(state_manager: Arc<StateManager>) -> Arc<GroupContext> {
1603 GroupContext::new(
1604 GroupId::new("RINCON_TEST123:1"),
1605 SpeakerId::new("RINCON_TEST123"),
1606 "192.168.1.100".parse().unwrap(),
1607 state_manager,
1608 SonosClient::new(),
1609 )
1610 }
1611
1612 #[test]
1613 fn test_group_property_handle_get_returns_none_initially() {
1614 let state_manager = create_test_state_manager();
1615 let context = create_test_group_context(state_manager);
1616
1617 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1618
1619 assert!(handle.get().is_none());
1620 }
1621
1622 #[test]
1623 fn test_group_property_handle_get_returns_cached_value() {
1624 let state_manager = create_test_state_manager();
1625 let group_id = GroupId::new("RINCON_TEST123:1");
1626
1627 state_manager.set_group_property(&group_id, GroupVolume::new(65));
1629
1630 let context = create_test_group_context(Arc::clone(&state_manager));
1631 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1632
1633 assert_eq!(handle.get(), Some(GroupVolume::new(65)));
1634 }
1635
1636 #[test]
1637 fn test_group_property_handle_watch_and_drop() {
1638 let state_manager = create_test_state_manager();
1639 let context = create_test_group_context(Arc::clone(&state_manager));
1640
1641 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1642
1643 assert!(!handle.is_watched());
1644 let wh = handle.watch().unwrap();
1645 assert!(handle.is_watched());
1646
1647 drop(wh);
1648 assert!(!handle.is_watched());
1649 }
1650
1651 #[test]
1656 fn test_group_handle_held_across_change_reports_new_value() {
1657 let state_manager = create_test_state_manager();
1658 let group_id = GroupId::new("RINCON_TEST123:1");
1659 let context = create_test_group_context(Arc::clone(&state_manager));
1660
1661 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1662
1663 let wh = handle.watch().unwrap();
1664 assert!(!wh.has_value(), "the group store is empty at this point");
1665
1666 state_manager.set_group_property(&group_id, GroupVolume::new(20));
1667 assert_eq!(
1668 wh.value(),
1669 Some(GroupVolume::new(20)),
1670 "a group handle must read the group store live, not a snapshot"
1671 );
1672
1673 state_manager.set_group_property(&group_id, GroupVolume::new(21));
1674 assert_eq!(wh.value(), Some(GroupVolume::new(21)));
1675 }
1676
1677 #[test]
1678 fn test_group_property_handle_group_id() {
1679 let state_manager = create_test_state_manager();
1680 let context = create_test_group_context(state_manager);
1681
1682 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1683
1684 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1685 }
1686
1687 #[test]
1688 fn test_group_mute_handle_accessible() {
1689 let state_manager = create_test_state_manager();
1690 let context = create_test_group_context(state_manager);
1691
1692 let handle: GroupMuteHandle = GroupPropertyHandle::new(context);
1693
1694 assert!(handle.get().is_none());
1695 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1696 }
1697
1698 #[test]
1699 fn test_group_volume_changeable_handle_accessible() {
1700 let state_manager = create_test_state_manager();
1701 let context = create_test_group_context(state_manager);
1702
1703 let handle: GroupVolumeChangeableHandle = GroupPropertyHandle::new(context);
1704
1705 assert!(handle.get().is_none());
1706 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1707 }
1708
1709 #[test]
1714 fn test_fetchable_impls_exist() {
1715 fn assert_fetchable<T: Fetchable>() {}
1716 assert_fetchable::<Volume>();
1717 assert_fetchable::<PlaybackState>();
1718 assert_fetchable::<Position>();
1719 assert_fetchable::<Mute>();
1720 assert_fetchable::<Bass>();
1721 assert_fetchable::<Treble>();
1722 assert_fetchable::<Loudness>();
1723 assert_fetchable::<CurrentTrack>();
1724 }
1725
1726 #[test]
1727 fn test_fetchable_with_context_impls_exist() {
1728 fn assert_fetchable_with_context<T: FetchableWithContext>() {}
1729 assert_fetchable_with_context::<GroupMembership>();
1730 }
1731
1732 #[test]
1733 fn test_group_fetchable_impls_exist() {
1734 fn assert_group_fetchable<T: GroupFetchable>() {}
1735 assert_group_fetchable::<GroupVolume>();
1736 assert_group_fetchable::<GroupMute>();
1737 }
1738}