1use std::fmt;
9use std::marker::PhantomData;
10use std::net::IpAddr;
11use std::ops::Deref;
12use std::sync::Arc;
13
14use sonos_api::operation::{ComposableOperation, UPnPOperation};
15use sonos_api::{ServiceScope, SonosClient};
16use sonos_event_manager::WatchGuard;
17use sonos_state::{property::SonosProperty, SpeakerId, StateManager};
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"]
117pub struct WatchHandle<P> {
118 value: Option<P>,
119 mode: WatchMode,
120 _cleanup: WatchCleanup,
121}
122
123impl<P> Deref for WatchHandle<P> {
124 type Target = Option<P>;
125 fn deref(&self) -> &Self::Target {
126 &self.value
127 }
128}
129
130impl<P> WatchHandle<P> {
131 pub fn mode(&self) -> WatchMode {
133 self.mode
134 }
135
136 pub fn value(&self) -> Option<&P> {
139 self.value.as_ref()
140 }
141
142 pub fn has_value(&self) -> bool {
144 self.value.is_some()
145 }
146
147 pub fn has_realtime_events(&self) -> bool {
149 self.mode == WatchMode::Events
150 }
151}
152
153impl<P: fmt::Debug> fmt::Debug for WatchHandle<P> {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 f.debug_struct("WatchHandle")
156 .field("value", &self.value)
157 .field("mode", &self.mode)
158 .finish()
159 }
160}
161
162#[allow(dead_code)]
173enum WatchCleanup {
174 Guard(WatchGuard),
175 CacheOnly(CacheOnlyGuard),
176 CoordinatorGuard {
177 _guard: WatchGuard,
178 _member_cleanup: CacheOnlyGuard,
179 },
180}
181
182struct CacheOnlyGuard {
190 state_manager: Arc<StateManager>,
191 speaker_id: SpeakerId,
192 property_key: &'static str,
193}
194
195impl Drop for CacheOnlyGuard {
196 fn drop(&mut self) {
197 self.state_manager
200 .unregister_watch(&self.speaker_id, self.property_key);
201 }
202}
203
204pub trait Fetchable: SonosProperty {
231 type Operation: UPnPOperation;
233
234 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
236
237 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
239}
240
241pub trait FetchableWithContext: SonosProperty {
246 type Operation: UPnPOperation;
248
249 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
251
252 fn from_response_with_context(
254 response: <Self::Operation as UPnPOperation>::Response,
255 speaker_id: &SpeakerId,
256 ) -> Option<Self>;
257}
258
259#[derive(Clone)]
284pub struct PropertyHandle<P: SonosProperty> {
285 context: Arc<SpeakerContext>,
286 _phantom: PhantomData<P>,
287}
288
289impl<P: SonosProperty> PropertyHandle<P> {
290 pub fn new(context: Arc<SpeakerContext>) -> Self {
292 Self {
293 context,
294 _phantom: PhantomData,
295 }
296 }
297
298 #[must_use = "returns the cached property value"]
311 pub fn get(&self) -> Option<P> {
312 self.context
313 .state_manager
314 .get_property::<P>(&self.context.speaker_id)
315 }
316
317 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
342 tracing::trace!(
343 "watch() called for {:?} on {}",
344 P::SERVICE,
345 self.context.speaker_id.as_str()
346 );
347
348 if self.context.state_manager.event_manager().is_none() {
350 if let Some(init) = self.context.state_manager.event_init() {
351 tracing::debug!(
352 "Event manager not initialized, triggering lazy init for {:?} on {}",
353 P::SERVICE,
354 self.context.speaker_id.as_str()
355 );
356 init().map_err(|e| SdkError::EventManager(e.to_string()))?;
357 } else {
358 tracing::debug!(
359 "No event_init closure available (test mode?) for {}",
360 self.context.speaker_id.as_str()
361 );
362 }
363 }
364
365 let (sub_id, sub_ip) = self.context.state_manager.resolve_subscription_target(
367 &self.context.speaker_id,
368 self.context.speaker_ip,
369 P::SERVICE,
370 );
371 let routed_to_coordinator = sub_id != self.context.speaker_id;
372
373 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
374 match em.acquire_watch(&sub_id, P::KEY, sub_ip, P::SERVICE) {
375 Ok(guard) => {
376 if routed_to_coordinator {
377 self.context
379 .state_manager
380 .register_watch(&self.context.speaker_id, P::KEY);
381 (
382 WatchMode::Events,
383 WatchCleanup::CoordinatorGuard {
384 _guard: guard,
385 _member_cleanup: CacheOnlyGuard {
386 state_manager: Arc::clone(&self.context.state_manager),
387 speaker_id: self.context.speaker_id.clone(),
388 property_key: P::KEY,
389 },
390 },
391 )
392 } else {
393 (WatchMode::Events, WatchCleanup::Guard(guard))
394 }
395 }
396 Err(e) => {
397 tracing::warn!(
398 "Failed to subscribe to {:?} for {}: {} - falling back to polling",
399 P::SERVICE,
400 self.context.speaker_id.as_str(),
401 e
402 );
403 self.context
405 .state_manager
406 .register_watch(&self.context.speaker_id, P::KEY);
407 (
408 WatchMode::Polling,
409 WatchCleanup::CacheOnly(CacheOnlyGuard {
410 state_manager: Arc::clone(&self.context.state_manager),
411 speaker_id: self.context.speaker_id.clone(),
412 property_key: P::KEY,
413 }),
414 )
415 }
416 }
417 } else {
418 tracing::warn!(
420 "No event manager available for {} — falling back to cache-only mode",
421 self.context.speaker_id.as_str()
422 );
423 self.context
424 .state_manager
425 .register_watch(&self.context.speaker_id, P::KEY);
426 (
427 WatchMode::CacheOnly,
428 WatchCleanup::CacheOnly(CacheOnlyGuard {
429 state_manager: Arc::clone(&self.context.state_manager),
430 speaker_id: self.context.speaker_id.clone(),
431 property_key: P::KEY,
432 }),
433 )
434 };
435
436 tracing::debug!(
437 "watch() resolved to {:?} for {} on {}",
438 mode,
439 P::KEY,
440 self.context.speaker_id.as_str()
441 );
442
443 Ok(WatchHandle {
444 value: self.get(),
445 mode,
446 _cleanup: cleanup,
447 })
448 }
449
450 #[must_use = "returns whether the property is being watched"]
466 pub fn is_watched(&self) -> bool {
467 self.context
468 .state_manager
469 .is_watched(&self.context.speaker_id, P::KEY)
470 }
471
472 pub fn speaker_id(&self) -> &SpeakerId {
474 &self.context.speaker_id
475 }
476
477 pub fn speaker_ip(&self) -> IpAddr {
479 self.context.speaker_ip
480 }
481}
482
483impl<P: Fetchable> PropertyHandle<P> {
488 pub fn watch_or_fetch(&self) -> Result<WatchHandle<P>, SdkError> {
494 let mut wh = self.watch()?;
495 if wh.value.is_none() {
496 match self.fetch() {
497 Ok(val) => wh.value = Some(val),
498 Err(e) => {
499 tracing::warn!("watch_or_fetch: fetch failed for {}: {e}", P::KEY);
500 }
501 }
502 }
503 Ok(wh)
504 }
505
506 #[must_use = "returns the fetched value from the device"]
522 pub fn fetch(&self) -> Result<P, SdkError> {
523 let operation = P::build_operation()?;
524
525 let (target_id, target_ip) = if P::SERVICE.scope() == ServiceScope::PerCoordinator {
527 self.context.state_manager.resolve_subscription_target(
528 &self.context.speaker_id,
529 self.context.speaker_ip,
530 P::SERVICE,
531 )
532 } else {
533 let current_ip = self
534 .context
535 .state_manager
536 .get_speaker_ip(&self.context.speaker_id)
537 .unwrap_or(self.context.speaker_ip);
538 (self.context.speaker_id.clone(), current_ip)
539 };
540
541 let response = self
542 .context
543 .api_client
544 .execute_enhanced(&target_ip.to_string(), operation)
545 .map_err(SdkError::ApiError)?;
546
547 let property_value = P::from_response(response);
548
549 self.context
551 .state_manager
552 .set_property(&target_id, property_value.clone());
553
554 Ok(property_value)
555 }
556}
557
558impl PropertyHandle<GroupMembership> {
567 #[must_use = "returns the fetched value from the device"]
572 pub fn fetch(&self) -> Result<GroupMembership, SdkError> {
573 let operation = <GroupMembership as FetchableWithContext>::build_operation()?;
574
575 let response = self
576 .context
577 .api_client
578 .execute_enhanced(&self.context.speaker_ip.to_string(), operation)
579 .map_err(SdkError::ApiError)?;
580
581 let property_value =
582 GroupMembership::from_response_with_context(response, &self.context.speaker_id)
583 .ok_or_else(|| {
584 SdkError::FetchFailed(format!(
585 "Speaker {} not found in topology response",
586 self.context.speaker_id.as_str()
587 ))
588 })?;
589
590 self.context
591 .state_manager
592 .set_property(&self.context.speaker_id, property_value.clone());
593
594 Ok(property_value)
595 }
596}
597
598use sonos_api::services::{
603 av_transport::{
604 self, GetPositionInfoOperation, GetPositionInfoResponse, GetTransportInfoOperation,
605 GetTransportInfoResponse,
606 },
607 group_rendering_control::{
608 self, GetGroupMuteOperation, GetGroupMuteResponse, GetGroupVolumeOperation,
609 GetGroupVolumeResponse,
610 },
611 rendering_control::{
612 self, GetBassOperation, GetBassResponse, GetLoudnessOperation, GetLoudnessResponse,
613 GetMuteOperation, GetMuteResponse, GetTrebleOperation, GetTrebleResponse,
614 GetVolumeOperation, GetVolumeResponse,
615 },
616 zone_group_topology::{self, GetZoneGroupStateOperation, GetZoneGroupStateResponse},
617};
618use sonos_state::{
619 Bass, CurrentTrack, GroupId, GroupMembership, GroupMute, GroupVolume, GroupVolumeChangeable,
620 Loudness, Mute, PlaybackState, Position, Treble, Volume,
621};
622
623fn build_error<E: std::fmt::Display>(operation_name: &str, e: E) -> SdkError {
629 SdkError::FetchFailed(format!("Failed to build {operation_name} operation: {e}"))
630}
631
632impl Fetchable for Volume {
637 type Operation = GetVolumeOperation;
638
639 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
640 rendering_control::get_volume_operation("Master".to_string())
641 .build()
642 .map_err(|e| build_error("GetVolume", e))
643 }
644
645 fn from_response(response: GetVolumeResponse) -> Self {
646 Volume::new(response.current_volume)
647 }
648}
649
650impl Fetchable for PlaybackState {
651 type Operation = GetTransportInfoOperation;
652
653 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
654 av_transport::get_transport_info_operation()
655 .build()
656 .map_err(|e| build_error("GetTransportInfo", e))
657 }
658
659 fn from_response(response: GetTransportInfoResponse) -> Self {
660 match response.current_transport_state.as_str() {
661 "PLAYING" => PlaybackState::Playing,
662 "PAUSED" | "PAUSED_PLAYBACK" => PlaybackState::Paused,
663 "STOPPED" => PlaybackState::Stopped,
664 _ => PlaybackState::Transitioning,
665 }
666 }
667}
668
669impl Fetchable for Position {
670 type Operation = GetPositionInfoOperation;
671
672 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
673 av_transport::get_position_info_operation()
674 .build()
675 .map_err(|e| build_error("GetPositionInfo", e))
676 }
677
678 fn from_response(response: GetPositionInfoResponse) -> Self {
679 let position_ms = Position::parse_time_to_ms(&response.rel_time).unwrap_or(0);
680 let duration_ms = Position::parse_time_to_ms(&response.track_duration).unwrap_or(0);
681 Position::new(position_ms, duration_ms)
682 }
683}
684
685impl Fetchable for Mute {
686 type Operation = GetMuteOperation;
687
688 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
689 rendering_control::get_mute_operation("Master".to_string())
690 .build()
691 .map_err(|e| build_error("GetMute", e))
692 }
693
694 fn from_response(response: GetMuteResponse) -> Self {
695 Mute::new(response.current_mute)
696 }
697}
698
699impl Fetchable for Bass {
700 type Operation = GetBassOperation;
701
702 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
703 rendering_control::get_bass_operation()
704 .build()
705 .map_err(|e| build_error("GetBass", e))
706 }
707
708 fn from_response(response: GetBassResponse) -> Self {
709 Bass::new(response.current_bass)
710 }
711}
712
713impl Fetchable for Treble {
714 type Operation = GetTrebleOperation;
715
716 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
717 rendering_control::get_treble_operation()
718 .build()
719 .map_err(|e| build_error("GetTreble", e))
720 }
721
722 fn from_response(response: GetTrebleResponse) -> Self {
723 Treble::new(response.current_treble)
724 }
725}
726
727impl Fetchable for Loudness {
728 type Operation = GetLoudnessOperation;
729
730 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
731 rendering_control::get_loudness_operation("Master".to_string())
732 .build()
733 .map_err(|e| build_error("GetLoudness", e))
734 }
735
736 fn from_response(response: GetLoudnessResponse) -> Self {
737 Loudness::new(response.current_loudness)
738 }
739}
740
741impl Fetchable for CurrentTrack {
742 type Operation = GetPositionInfoOperation;
743
744 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
745 av_transport::get_position_info_operation()
746 .build()
747 .map_err(|e| build_error("GetPositionInfo", e))
748 }
749
750 fn from_response(response: GetPositionInfoResponse) -> Self {
751 let metadata = if response.track_meta_data.is_empty()
752 || response.track_meta_data == "NOT_IMPLEMENTED"
753 {
754 None
755 } else {
756 Some(response.track_meta_data.as_str())
757 };
758 let (title, artist, album, album_art_uri) = sonos_state::parse_track_metadata(metadata);
759 CurrentTrack {
760 title,
761 artist,
762 album,
763 album_art_uri,
764 uri: Some(response.track_uri).filter(|s| !s.is_empty()),
765 }
766 }
767}
768
769impl FetchableWithContext for GroupMembership {
774 type Operation = GetZoneGroupStateOperation;
775
776 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
777 zone_group_topology::get_zone_group_state_operation()
778 .build()
779 .map_err(|e| build_error("GetZoneGroupState", e))
780 }
781
782 fn from_response_with_context(
783 response: GetZoneGroupStateResponse,
784 speaker_id: &SpeakerId,
785 ) -> Option<Self> {
786 let zone_groups =
787 zone_group_topology::parse_zone_group_state_xml(&response.zone_group_state).ok()?;
788
789 for group in &zone_groups {
790 let is_member = group.members.iter().any(|m| m.uuid == speaker_id.as_str());
791 if is_member {
792 let is_coordinator = group.coordinator == speaker_id.as_str();
793 return Some(GroupMembership::new(
794 GroupId::new(&group.id),
795 is_coordinator,
796 ));
797 }
798 }
799
800 None
801 }
802}
803
804pub type VolumeHandle = PropertyHandle<Volume>;
821
822pub type PlaybackStateHandle = PropertyHandle<PlaybackState>;
824
825pub type MuteHandle = PropertyHandle<Mute>;
827
828pub type BassHandle = PropertyHandle<Bass>;
830
831pub type TrebleHandle = PropertyHandle<Treble>;
833
834pub type LoudnessHandle = PropertyHandle<Loudness>;
836
837pub type PositionHandle = PropertyHandle<Position>;
839
840pub type CurrentTrackHandle = PropertyHandle<CurrentTrack>;
842
843pub type GroupMembershipHandle = PropertyHandle<GroupMembership>;
845
846#[derive(Clone)]
855pub struct GroupContext {
856 pub(crate) group_id: GroupId,
857 pub(crate) coordinator_id: SpeakerId,
858 pub(crate) coordinator_ip: IpAddr,
859 pub(crate) state_manager: Arc<StateManager>,
860 pub(crate) api_client: SonosClient,
861}
862
863impl GroupContext {
864 pub fn new(
866 group_id: GroupId,
867 coordinator_id: SpeakerId,
868 coordinator_ip: IpAddr,
869 state_manager: Arc<StateManager>,
870 api_client: SonosClient,
871 ) -> Arc<Self> {
872 Arc::new(Self {
873 group_id,
874 coordinator_id,
875 coordinator_ip,
876 state_manager,
877 api_client,
878 })
879 }
880}
881
882#[derive(Clone)]
888pub struct GroupPropertyHandle<P: SonosProperty> {
889 context: Arc<GroupContext>,
890 _phantom: PhantomData<P>,
891}
892
893impl<P: SonosProperty> GroupPropertyHandle<P> {
894 pub fn new(context: Arc<GroupContext>) -> Self {
896 Self {
897 context,
898 _phantom: PhantomData,
899 }
900 }
901
902 #[must_use = "returns the cached property value"]
904 pub fn get(&self) -> Option<P> {
905 self.context
906 .state_manager
907 .get_group_property::<P>(&self.context.group_id)
908 }
909
910 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
915 if self.context.state_manager.event_manager().is_none() {
917 if let Some(init) = self.context.state_manager.event_init() {
918 tracing::debug!(
919 "Event manager not initialized, triggering lazy init for group {:?} on {}",
920 P::SERVICE,
921 self.context.group_id.as_str()
922 );
923 init().map_err(|e| SdkError::EventManager(e.to_string()))?;
924 } else {
925 tracing::debug!(
926 "No event_init closure available (test mode?) for group {}",
927 self.context.group_id.as_str()
928 );
929 }
930 }
931
932 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
933 match em.acquire_watch(
934 &self.context.coordinator_id,
935 P::KEY,
936 self.context.coordinator_ip,
937 P::SERVICE,
938 ) {
939 Ok(guard) => (WatchMode::Events, WatchCleanup::Guard(guard)),
940 Err(e) => {
941 tracing::warn!(
942 "Failed to subscribe to {:?} for group {}: {} - falling back to polling",
943 P::SERVICE,
944 self.context.group_id.as_str(),
945 e
946 );
947 self.context
948 .state_manager
949 .register_watch(&self.context.coordinator_id, P::KEY);
950 (
951 WatchMode::Polling,
952 WatchCleanup::CacheOnly(CacheOnlyGuard {
953 state_manager: Arc::clone(&self.context.state_manager),
954 speaker_id: self.context.coordinator_id.clone(),
955 property_key: P::KEY,
956 }),
957 )
958 }
959 }
960 } else {
961 self.context
962 .state_manager
963 .register_watch(&self.context.coordinator_id, P::KEY);
964 (
965 WatchMode::CacheOnly,
966 WatchCleanup::CacheOnly(CacheOnlyGuard {
967 state_manager: Arc::clone(&self.context.state_manager),
968 speaker_id: self.context.coordinator_id.clone(),
969 property_key: P::KEY,
970 }),
971 )
972 };
973
974 Ok(WatchHandle {
975 value: self.get(),
976 mode,
977 _cleanup: cleanup,
978 })
979 }
980
981 #[must_use = "returns whether the property is being watched"]
983 pub fn is_watched(&self) -> bool {
984 self.context
985 .state_manager
986 .is_watched(&self.context.coordinator_id, P::KEY)
987 }
988
989 pub fn group_id(&self) -> &GroupId {
991 &self.context.group_id
992 }
993}
994
995pub trait GroupFetchable: SonosProperty {
997 type Operation: UPnPOperation;
999
1000 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
1002
1003 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
1005}
1006
1007impl<P: GroupFetchable> GroupPropertyHandle<P> {
1008 pub fn watch_or_fetch(&self) -> Result<WatchHandle<P>, SdkError> {
1011 let mut wh = self.watch()?;
1012 if wh.value.is_none() {
1013 match self.fetch() {
1014 Ok(val) => wh.value = Some(val),
1015 Err(e) => {
1016 tracing::warn!(
1017 "watch_or_fetch: fetch failed for group {} {}: {e}",
1018 self.context.group_id.as_str(),
1019 P::KEY
1020 );
1021 }
1022 }
1023 }
1024 Ok(wh)
1025 }
1026
1027 #[must_use = "returns the fetched value from the device"]
1029 pub fn fetch(&self) -> Result<P, SdkError> {
1030 let operation = P::build_operation()?;
1031
1032 let response = self
1033 .context
1034 .api_client
1035 .execute_enhanced(&self.context.coordinator_ip.to_string(), operation)
1036 .map_err(SdkError::ApiError)?;
1037
1038 let property_value = P::from_response(response);
1039
1040 self.context
1041 .state_manager
1042 .set_group_property(&self.context.group_id, property_value.clone());
1043
1044 Ok(property_value)
1045 }
1046}
1047
1048impl GroupFetchable for GroupVolume {
1053 type Operation = GetGroupVolumeOperation;
1054
1055 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
1056 group_rendering_control::get_group_volume()
1057 .build()
1058 .map_err(|e| build_error("GetGroupVolume", e))
1059 }
1060
1061 fn from_response(response: GetGroupVolumeResponse) -> Self {
1062 GroupVolume::new(response.current_volume)
1063 }
1064}
1065
1066impl GroupFetchable for GroupMute {
1067 type Operation = GetGroupMuteOperation;
1068
1069 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
1070 group_rendering_control::get_group_mute()
1071 .build()
1072 .map_err(|e| build_error("GetGroupMute", e))
1073 }
1074
1075 fn from_response(response: GetGroupMuteResponse) -> Self {
1076 GroupMute::new(response.current_mute)
1077 }
1078}
1079
1080pub type GroupVolumeHandle = GroupPropertyHandle<GroupVolume>;
1086
1087pub type GroupMuteHandle = GroupPropertyHandle<GroupMute>;
1089
1090pub type GroupVolumeChangeableHandle = GroupPropertyHandle<GroupVolumeChangeable>;
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096 use sonos_discovery::Device;
1097 use sonos_state::Property;
1098
1099 fn create_test_state_manager() -> Arc<StateManager> {
1100 let manager = StateManager::new().unwrap();
1101 let devices = vec![Device {
1102 id: "RINCON_TEST123".to_string(),
1103 name: "Test Speaker".to_string(),
1104 room_name: "Test Room".to_string(),
1105 ip_address: "192.168.1.100".to_string(),
1106 port: 1400,
1107 model_name: "Sonos One".to_string(),
1108 }];
1109 manager.add_devices(devices).unwrap();
1110 Arc::new(manager)
1111 }
1112
1113 fn create_test_context(state_manager: Arc<StateManager>) -> Arc<SpeakerContext> {
1114 SpeakerContext::new(
1115 SpeakerId::new("RINCON_TEST123"),
1116 "192.168.1.100".parse().unwrap(),
1117 state_manager,
1118 SonosClient::new(),
1119 )
1120 }
1121
1122 #[test]
1123 fn test_property_handle_creation() {
1124 let state_manager = create_test_state_manager();
1125 let context = create_test_context(state_manager);
1126 let speaker_ip: IpAddr = "192.168.1.100".parse().unwrap();
1127
1128 let handle: VolumeHandle = PropertyHandle::new(context);
1129
1130 assert_eq!(handle.speaker_id().as_str(), "RINCON_TEST123");
1131 assert_eq!(handle.speaker_ip(), speaker_ip);
1132 }
1133
1134 #[test]
1135 fn test_get_returns_none_initially() {
1136 let state_manager = create_test_state_manager();
1137 let context = create_test_context(state_manager);
1138
1139 let handle: VolumeHandle = PropertyHandle::new(context);
1140
1141 assert!(handle.get().is_none());
1142 }
1143
1144 #[test]
1145 fn test_get_returns_cached_value() {
1146 let state_manager = create_test_state_manager();
1147 let speaker_id = SpeakerId::new("RINCON_TEST123");
1148
1149 state_manager.set_property(&speaker_id, Volume::new(75));
1150
1151 let context = create_test_context(Arc::clone(&state_manager));
1152 let handle: VolumeHandle = PropertyHandle::new(context);
1153
1154 assert_eq!(handle.get(), Some(Volume::new(75)));
1155 }
1156
1157 #[test]
1158 fn test_watch_registers_property() {
1159 let state_manager = create_test_state_manager();
1160 let context = create_test_context(Arc::clone(&state_manager));
1161
1162 let handle: VolumeHandle = PropertyHandle::new(context);
1163
1164 assert!(!handle.is_watched());
1165 let _wh = handle.watch().unwrap();
1166 assert!(handle.is_watched());
1167 }
1168
1169 #[test]
1170 fn test_drop_watch_handle_unregisters_property() {
1171 let state_manager = create_test_state_manager();
1172 let context = create_test_context(Arc::clone(&state_manager));
1173
1174 let handle: VolumeHandle = PropertyHandle::new(context);
1175
1176 let wh = handle.watch().unwrap();
1177 assert!(handle.is_watched());
1178
1179 drop(wh);
1180 assert!(!handle.is_watched());
1181 }
1182
1183 #[test]
1196 fn test_dropping_one_of_two_handles_keeps_property_emitting() {
1197 let state_manager = create_test_state_manager();
1198 let speaker_id = SpeakerId::new("RINCON_TEST123");
1199 let context = create_test_context(Arc::clone(&state_manager));
1200
1201 let volume: VolumeHandle = PropertyHandle::new(Arc::clone(&context));
1202 let mute: MuteHandle = PropertyHandle::new(context);
1203
1204 let first = volume.watch().unwrap();
1205 let second = volume.watch().unwrap();
1206 let _mute_watch = mute.watch().unwrap();
1208 assert!(volume.is_watched());
1209 assert!(mute.is_watched());
1210
1211 drop(first);
1212
1213 assert!(
1214 volume.is_watched(),
1215 "one of two Volume handles dropped — the property must stay watched"
1216 );
1217 assert!(
1218 mute.is_watched(),
1219 "releasing a Volume handle must not disturb its RenderingControl sibling"
1220 );
1221
1222 state_manager.set_property(&speaker_id, Volume::new(11));
1224 state_manager.set_property(&speaker_id, Mute::new(true));
1225
1226 let iter = state_manager.iter();
1227 let first_event = iter
1228 .recv_timeout(std::time::Duration::from_millis(100))
1229 .expect("Volume is still held by `second` and must still emit");
1230 assert_eq!(first_event.property_key, Volume::KEY);
1231 let second_event = iter
1232 .recv_timeout(std::time::Duration::from_millis(100))
1233 .expect("Mute is still held and must still emit");
1234 assert_eq!(second_event.property_key, Mute::KEY);
1235
1236 drop(second);
1238 assert!(!volume.is_watched());
1239 state_manager.set_property(&speaker_id, Volume::new(22));
1240 assert!(
1241 iter.recv_timeout(std::time::Duration::from_millis(50))
1242 .is_none(),
1243 "with every Volume handle dropped the property must stop emitting"
1244 );
1245 }
1246
1247 #[test]
1248 fn test_watch_returns_current_value() {
1249 let state_manager = create_test_state_manager();
1250 let speaker_id = SpeakerId::new("RINCON_TEST123");
1251
1252 state_manager.set_property(&speaker_id, Volume::new(50));
1253
1254 let context = create_test_context(Arc::clone(&state_manager));
1255 let handle: VolumeHandle = PropertyHandle::new(context);
1256
1257 let wh = handle.watch().unwrap();
1258 assert_eq!(*wh, Some(Volume::new(50)));
1259 assert_eq!(wh.value(), Some(&Volume::new(50)));
1260 assert_eq!(wh.mode(), WatchMode::CacheOnly);
1262 }
1263
1264 #[test]
1265 fn test_watch_handle_deref() {
1266 let state_manager = create_test_state_manager();
1267 let speaker_id = SpeakerId::new("RINCON_TEST123");
1268
1269 state_manager.set_property(&speaker_id, Volume::new(75));
1270
1271 let context = create_test_context(Arc::clone(&state_manager));
1272 let handle: VolumeHandle = PropertyHandle::new(context);
1273
1274 let wh = handle.watch().unwrap();
1275 assert!(wh.has_value());
1277 assert!(!wh.has_realtime_events());
1278 if let Some(v) = &*wh {
1279 assert_eq!(v.value(), 75);
1280 } else {
1281 panic!("Expected Some value");
1282 }
1283 }
1284
1285 #[test]
1286 fn test_property_handle_clone() {
1287 let state_manager = create_test_state_manager();
1288 let speaker_id = SpeakerId::new("RINCON_TEST123");
1289
1290 state_manager.set_property(&speaker_id, Volume::new(60));
1291
1292 let context = create_test_context(Arc::clone(&state_manager));
1293 let handle: VolumeHandle = PropertyHandle::new(context);
1294
1295 let cloned = handle.clone();
1296
1297 assert_eq!(handle.get(), cloned.get());
1298 assert_eq!(handle.get(), Some(Volume::new(60)));
1299 }
1300
1301 fn create_test_group_context(state_manager: Arc<StateManager>) -> Arc<GroupContext> {
1306 GroupContext::new(
1307 GroupId::new("RINCON_TEST123:1"),
1308 SpeakerId::new("RINCON_TEST123"),
1309 "192.168.1.100".parse().unwrap(),
1310 state_manager,
1311 SonosClient::new(),
1312 )
1313 }
1314
1315 #[test]
1316 fn test_group_property_handle_get_returns_none_initially() {
1317 let state_manager = create_test_state_manager();
1318 let context = create_test_group_context(state_manager);
1319
1320 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1321
1322 assert!(handle.get().is_none());
1323 }
1324
1325 #[test]
1326 fn test_group_property_handle_get_returns_cached_value() {
1327 let state_manager = create_test_state_manager();
1328 let group_id = GroupId::new("RINCON_TEST123:1");
1329
1330 state_manager.set_group_property(&group_id, GroupVolume::new(65));
1332
1333 let context = create_test_group_context(Arc::clone(&state_manager));
1334 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1335
1336 assert_eq!(handle.get(), Some(GroupVolume::new(65)));
1337 }
1338
1339 #[test]
1340 fn test_group_property_handle_watch_and_drop() {
1341 let state_manager = create_test_state_manager();
1342 let context = create_test_group_context(Arc::clone(&state_manager));
1343
1344 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1345
1346 assert!(!handle.is_watched());
1347 let wh = handle.watch().unwrap();
1348 assert!(handle.is_watched());
1349
1350 drop(wh);
1351 assert!(!handle.is_watched());
1352 }
1353
1354 #[test]
1355 fn test_group_property_handle_group_id() {
1356 let state_manager = create_test_state_manager();
1357 let context = create_test_group_context(state_manager);
1358
1359 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1360
1361 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1362 }
1363
1364 #[test]
1365 fn test_group_mute_handle_accessible() {
1366 let state_manager = create_test_state_manager();
1367 let context = create_test_group_context(state_manager);
1368
1369 let handle: GroupMuteHandle = GroupPropertyHandle::new(context);
1370
1371 assert!(handle.get().is_none());
1372 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1373 }
1374
1375 #[test]
1376 fn test_group_volume_changeable_handle_accessible() {
1377 let state_manager = create_test_state_manager();
1378 let context = create_test_group_context(state_manager);
1379
1380 let handle: GroupVolumeChangeableHandle = GroupPropertyHandle::new(context);
1381
1382 assert!(handle.get().is_none());
1383 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1384 }
1385
1386 #[test]
1391 fn test_fetchable_impls_exist() {
1392 fn assert_fetchable<T: Fetchable>() {}
1393 assert_fetchable::<Volume>();
1394 assert_fetchable::<PlaybackState>();
1395 assert_fetchable::<Position>();
1396 assert_fetchable::<Mute>();
1397 assert_fetchable::<Bass>();
1398 assert_fetchable::<Treble>();
1399 assert_fetchable::<Loudness>();
1400 assert_fetchable::<CurrentTrack>();
1401 }
1402
1403 #[test]
1404 fn test_fetchable_with_context_impls_exist() {
1405 fn assert_fetchable_with_context<T: FetchableWithContext>() {}
1406 assert_fetchable_with_context::<GroupMembership>();
1407 }
1408
1409 #[test]
1410 fn test_group_fetchable_impls_exist() {
1411 fn assert_group_fetchable<T: GroupFetchable>() {}
1412 assert_group_fetchable::<GroupVolume>();
1413 assert_group_fetchable::<GroupMute>();
1414 }
1415}