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::SonosClient;
16use sonos_event_manager::WatchGuard;
17use sonos_state::{property::SonosProperty, SpeakerId, StateManager};
18
19use crate::SdkError;
20
21pub type EventInitFn = Arc<dyn Fn() -> Result<(), SdkError> + Send + Sync>;
27
28#[derive(Clone)]
33pub struct SpeakerContext {
34 pub(crate) speaker_id: SpeakerId,
35 pub(crate) speaker_ip: IpAddr,
36 pub(crate) state_manager: Arc<StateManager>,
37 pub(crate) api_client: SonosClient,
38 pub(crate) event_init: Option<EventInitFn>,
41}
42
43impl SpeakerContext {
44 pub fn new(
46 speaker_id: SpeakerId,
47 speaker_ip: IpAddr,
48 state_manager: Arc<StateManager>,
49 api_client: SonosClient,
50 ) -> Arc<Self> {
51 Arc::new(Self {
52 speaker_id,
53 speaker_ip,
54 state_manager,
55 api_client,
56 event_init: None,
57 })
58 }
59
60 pub fn with_event_init(
62 speaker_id: SpeakerId,
63 speaker_ip: IpAddr,
64 state_manager: Arc<StateManager>,
65 api_client: SonosClient,
66 event_init: EventInitFn,
67 ) -> Arc<Self> {
68 Arc::new(Self {
69 speaker_id,
70 speaker_ip,
71 state_manager,
72 api_client,
73 event_init: Some(event_init),
74 })
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub enum WatchMode {
88 Events,
93
94 Polling,
100
101 CacheOnly,
106}
107
108impl fmt::Display for WatchMode {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 match self {
111 WatchMode::Events => write!(f, "Events (real-time)"),
112 WatchMode::Polling => write!(f, "Polling (fallback)"),
113 WatchMode::CacheOnly => write!(f, "CacheOnly (no events)"),
114 }
115 }
116}
117
118#[must_use = "dropping the handle starts the grace period — hold it to keep the subscription alive"]
145pub struct WatchHandle<P> {
146 value: Option<P>,
147 mode: WatchMode,
148 _cleanup: WatchCleanup,
149}
150
151impl<P> Deref for WatchHandle<P> {
152 type Target = Option<P>;
153 fn deref(&self) -> &Self::Target {
154 &self.value
155 }
156}
157
158impl<P> WatchHandle<P> {
159 pub fn mode(&self) -> WatchMode {
161 self.mode
162 }
163
164 pub fn value(&self) -> Option<&P> {
167 self.value.as_ref()
168 }
169
170 pub fn has_value(&self) -> bool {
172 self.value.is_some()
173 }
174
175 pub fn has_realtime_events(&self) -> bool {
177 self.mode == WatchMode::Events
178 }
179}
180
181impl<P: fmt::Debug> fmt::Debug for WatchHandle<P> {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 f.debug_struct("WatchHandle")
184 .field("value", &self.value)
185 .field("mode", &self.mode)
186 .finish()
187 }
188}
189
190#[allow(dead_code)]
198enum WatchCleanup {
199 Guard(WatchGuard),
200 CacheOnly(CacheOnlyGuard),
201}
202
203struct CacheOnlyGuard {
206 state_manager: Arc<StateManager>,
207 speaker_id: SpeakerId,
208 property_key: &'static str,
209}
210
211impl Drop for CacheOnlyGuard {
212 fn drop(&mut self) {
213 self.state_manager
214 .unregister_watch(&self.speaker_id, self.property_key);
215 }
216}
217
218pub trait Fetchable: SonosProperty {
245 type Operation: UPnPOperation;
247
248 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
250
251 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
253}
254
255pub trait FetchableWithContext: SonosProperty {
260 type Operation: UPnPOperation;
262
263 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
265
266 fn from_response_with_context(
268 response: <Self::Operation as UPnPOperation>::Response,
269 speaker_id: &SpeakerId,
270 ) -> Option<Self>;
271}
272
273#[derive(Clone)]
298pub struct PropertyHandle<P: SonosProperty> {
299 context: Arc<SpeakerContext>,
300 _phantom: PhantomData<P>,
301}
302
303impl<P: SonosProperty> PropertyHandle<P> {
304 pub fn new(context: Arc<SpeakerContext>) -> Self {
306 Self {
307 context,
308 _phantom: PhantomData,
309 }
310 }
311
312 #[must_use = "returns the cached property value"]
325 pub fn get(&self) -> Option<P> {
326 self.context
327 .state_manager
328 .get_property::<P>(&self.context.speaker_id)
329 }
330
331 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
356 if self.context.state_manager.event_manager().is_none() {
358 if let Some(ref init) = self.context.event_init {
359 init()?;
360 }
361 }
362
363 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
364 match em.acquire_watch(
365 &self.context.speaker_id,
366 P::KEY,
367 self.context.speaker_ip,
368 P::SERVICE,
369 ) {
370 Ok(guard) => (WatchMode::Events, WatchCleanup::Guard(guard)),
371 Err(e) => {
372 tracing::warn!(
373 "Failed to subscribe to {:?} for {}: {} - falling back to polling",
374 P::SERVICE,
375 self.context.speaker_id.as_str(),
376 e
377 );
378 self.context
380 .state_manager
381 .register_watch(&self.context.speaker_id, P::KEY);
382 (
383 WatchMode::Polling,
384 WatchCleanup::CacheOnly(CacheOnlyGuard {
385 state_manager: Arc::clone(&self.context.state_manager),
386 speaker_id: self.context.speaker_id.clone(),
387 property_key: P::KEY,
388 }),
389 )
390 }
391 }
392 } else {
393 self.context
395 .state_manager
396 .register_watch(&self.context.speaker_id, P::KEY);
397 (
398 WatchMode::CacheOnly,
399 WatchCleanup::CacheOnly(CacheOnlyGuard {
400 state_manager: Arc::clone(&self.context.state_manager),
401 speaker_id: self.context.speaker_id.clone(),
402 property_key: P::KEY,
403 }),
404 )
405 };
406
407 Ok(WatchHandle {
408 value: self.get(),
409 mode,
410 _cleanup: cleanup,
411 })
412 }
413
414 #[must_use = "returns whether the property is being watched"]
429 pub fn is_watched(&self) -> bool {
430 self.context
431 .state_manager
432 .is_watched(&self.context.speaker_id, P::KEY)
433 }
434
435 pub fn speaker_id(&self) -> &SpeakerId {
437 &self.context.speaker_id
438 }
439
440 pub fn speaker_ip(&self) -> IpAddr {
442 self.context.speaker_ip
443 }
444}
445
446impl<P: Fetchable> PropertyHandle<P> {
451 #[must_use = "returns the fetched value from the device"]
467 pub fn fetch(&self) -> Result<P, SdkError> {
468 let operation = P::build_operation()?;
470
471 let response = self
473 .context
474 .api_client
475 .execute_enhanced(&self.context.speaker_ip.to_string(), operation)
476 .map_err(SdkError::ApiError)?;
477
478 let property_value = P::from_response(response);
480
481 self.context
483 .state_manager
484 .set_property(&self.context.speaker_id, property_value.clone());
485
486 Ok(property_value)
487 }
488}
489
490impl PropertyHandle<GroupMembership> {
499 #[must_use = "returns the fetched value from the device"]
504 pub fn fetch(&self) -> Result<GroupMembership, SdkError> {
505 let operation = <GroupMembership as FetchableWithContext>::build_operation()?;
506
507 let response = self
508 .context
509 .api_client
510 .execute_enhanced(&self.context.speaker_ip.to_string(), operation)
511 .map_err(SdkError::ApiError)?;
512
513 let property_value =
514 GroupMembership::from_response_with_context(response, &self.context.speaker_id)
515 .ok_or_else(|| {
516 SdkError::FetchFailed(format!(
517 "Speaker {} not found in topology response",
518 self.context.speaker_id.as_str()
519 ))
520 })?;
521
522 self.context
523 .state_manager
524 .set_property(&self.context.speaker_id, property_value.clone());
525
526 Ok(property_value)
527 }
528}
529
530use sonos_api::services::{
535 av_transport::{
536 self, GetPositionInfoOperation, GetPositionInfoResponse, GetTransportInfoOperation,
537 GetTransportInfoResponse,
538 },
539 group_rendering_control::{
540 self, GetGroupMuteOperation, GetGroupMuteResponse, GetGroupVolumeOperation,
541 GetGroupVolumeResponse,
542 },
543 rendering_control::{
544 self, GetBassOperation, GetBassResponse, GetLoudnessOperation, GetLoudnessResponse,
545 GetMuteOperation, GetMuteResponse, GetTrebleOperation, GetTrebleResponse,
546 GetVolumeOperation, GetVolumeResponse,
547 },
548 zone_group_topology::{self, GetZoneGroupStateOperation, GetZoneGroupStateResponse},
549};
550use sonos_state::{
551 Bass, CurrentTrack, GroupId, GroupMembership, GroupMute, GroupVolume, GroupVolumeChangeable,
552 Loudness, Mute, PlaybackState, Position, Treble, Volume,
553};
554
555fn build_error<E: std::fmt::Display>(operation_name: &str, e: E) -> SdkError {
561 SdkError::FetchFailed(format!("Failed to build {operation_name} operation: {e}"))
562}
563
564impl Fetchable for Volume {
569 type Operation = GetVolumeOperation;
570
571 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
572 rendering_control::get_volume_operation("Master".to_string())
573 .build()
574 .map_err(|e| build_error("GetVolume", e))
575 }
576
577 fn from_response(response: GetVolumeResponse) -> Self {
578 Volume::new(response.current_volume)
579 }
580}
581
582impl Fetchable for PlaybackState {
583 type Operation = GetTransportInfoOperation;
584
585 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
586 av_transport::get_transport_info_operation()
587 .build()
588 .map_err(|e| build_error("GetTransportInfo", e))
589 }
590
591 fn from_response(response: GetTransportInfoResponse) -> Self {
592 match response.current_transport_state.as_str() {
593 "PLAYING" => PlaybackState::Playing,
594 "PAUSED" | "PAUSED_PLAYBACK" => PlaybackState::Paused,
595 "STOPPED" => PlaybackState::Stopped,
596 _ => PlaybackState::Transitioning,
597 }
598 }
599}
600
601impl Fetchable for Position {
602 type Operation = GetPositionInfoOperation;
603
604 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
605 av_transport::get_position_info_operation()
606 .build()
607 .map_err(|e| build_error("GetPositionInfo", e))
608 }
609
610 fn from_response(response: GetPositionInfoResponse) -> Self {
611 let position_ms = Position::parse_time_to_ms(&response.rel_time).unwrap_or(0);
612 let duration_ms = Position::parse_time_to_ms(&response.track_duration).unwrap_or(0);
613 Position::new(position_ms, duration_ms)
614 }
615}
616
617impl Fetchable for Mute {
618 type Operation = GetMuteOperation;
619
620 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
621 rendering_control::get_mute_operation("Master".to_string())
622 .build()
623 .map_err(|e| build_error("GetMute", e))
624 }
625
626 fn from_response(response: GetMuteResponse) -> Self {
627 Mute::new(response.current_mute)
628 }
629}
630
631impl Fetchable for Bass {
632 type Operation = GetBassOperation;
633
634 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
635 rendering_control::get_bass_operation()
636 .build()
637 .map_err(|e| build_error("GetBass", e))
638 }
639
640 fn from_response(response: GetBassResponse) -> Self {
641 Bass::new(response.current_bass)
642 }
643}
644
645impl Fetchable for Treble {
646 type Operation = GetTrebleOperation;
647
648 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
649 rendering_control::get_treble_operation()
650 .build()
651 .map_err(|e| build_error("GetTreble", e))
652 }
653
654 fn from_response(response: GetTrebleResponse) -> Self {
655 Treble::new(response.current_treble)
656 }
657}
658
659impl Fetchable for Loudness {
660 type Operation = GetLoudnessOperation;
661
662 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
663 rendering_control::get_loudness_operation("Master".to_string())
664 .build()
665 .map_err(|e| build_error("GetLoudness", e))
666 }
667
668 fn from_response(response: GetLoudnessResponse) -> Self {
669 Loudness::new(response.current_loudness)
670 }
671}
672
673impl Fetchable for CurrentTrack {
674 type Operation = GetPositionInfoOperation;
675
676 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
677 av_transport::get_position_info_operation()
678 .build()
679 .map_err(|e| build_error("GetPositionInfo", e))
680 }
681
682 fn from_response(response: GetPositionInfoResponse) -> Self {
683 let metadata = if response.track_meta_data.is_empty()
684 || response.track_meta_data == "NOT_IMPLEMENTED"
685 {
686 None
687 } else {
688 Some(response.track_meta_data.as_str())
689 };
690 let (title, artist, album, album_art_uri) = sonos_state::parse_track_metadata(metadata);
691 CurrentTrack {
692 title,
693 artist,
694 album,
695 album_art_uri,
696 uri: Some(response.track_uri).filter(|s| !s.is_empty()),
697 }
698 }
699}
700
701impl FetchableWithContext for GroupMembership {
706 type Operation = GetZoneGroupStateOperation;
707
708 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
709 zone_group_topology::get_zone_group_state_operation()
710 .build()
711 .map_err(|e| build_error("GetZoneGroupState", e))
712 }
713
714 fn from_response_with_context(
715 response: GetZoneGroupStateResponse,
716 speaker_id: &SpeakerId,
717 ) -> Option<Self> {
718 let zone_groups =
719 zone_group_topology::parse_zone_group_state_xml(&response.zone_group_state).ok()?;
720
721 for group in &zone_groups {
722 let is_member = group.members.iter().any(|m| m.uuid == speaker_id.as_str());
723 if is_member {
724 let is_coordinator = group.coordinator == speaker_id.as_str();
725 return Some(GroupMembership::new(
726 GroupId::new(&group.id),
727 is_coordinator,
728 ));
729 }
730 }
731
732 None
733 }
734}
735
736pub type VolumeHandle = PropertyHandle<Volume>;
753
754pub type PlaybackStateHandle = PropertyHandle<PlaybackState>;
756
757pub type MuteHandle = PropertyHandle<Mute>;
759
760pub type BassHandle = PropertyHandle<Bass>;
762
763pub type TrebleHandle = PropertyHandle<Treble>;
765
766pub type LoudnessHandle = PropertyHandle<Loudness>;
768
769pub type PositionHandle = PropertyHandle<Position>;
771
772pub type CurrentTrackHandle = PropertyHandle<CurrentTrack>;
774
775pub type GroupMembershipHandle = PropertyHandle<GroupMembership>;
777
778#[derive(Clone)]
787pub struct GroupContext {
788 pub(crate) group_id: GroupId,
789 pub(crate) coordinator_id: SpeakerId,
790 pub(crate) coordinator_ip: IpAddr,
791 pub(crate) state_manager: Arc<StateManager>,
792 pub(crate) api_client: SonosClient,
793}
794
795impl GroupContext {
796 pub fn new(
798 group_id: GroupId,
799 coordinator_id: SpeakerId,
800 coordinator_ip: IpAddr,
801 state_manager: Arc<StateManager>,
802 api_client: SonosClient,
803 ) -> Arc<Self> {
804 Arc::new(Self {
805 group_id,
806 coordinator_id,
807 coordinator_ip,
808 state_manager,
809 api_client,
810 })
811 }
812}
813
814#[derive(Clone)]
820pub struct GroupPropertyHandle<P: SonosProperty> {
821 context: Arc<GroupContext>,
822 _phantom: PhantomData<P>,
823}
824
825impl<P: SonosProperty> GroupPropertyHandle<P> {
826 pub fn new(context: Arc<GroupContext>) -> Self {
828 Self {
829 context,
830 _phantom: PhantomData,
831 }
832 }
833
834 #[must_use = "returns the cached property value"]
836 pub fn get(&self) -> Option<P> {
837 self.context
838 .state_manager
839 .get_group_property::<P>(&self.context.group_id)
840 }
841
842 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
847 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
848 match em.acquire_watch(
849 &self.context.coordinator_id,
850 P::KEY,
851 self.context.coordinator_ip,
852 P::SERVICE,
853 ) {
854 Ok(guard) => (WatchMode::Events, WatchCleanup::Guard(guard)),
855 Err(e) => {
856 tracing::warn!(
857 "Failed to subscribe to {:?} for group {}: {} - falling back to polling",
858 P::SERVICE,
859 self.context.group_id.as_str(),
860 e
861 );
862 self.context
863 .state_manager
864 .register_watch(&self.context.coordinator_id, P::KEY);
865 (
866 WatchMode::Polling,
867 WatchCleanup::CacheOnly(CacheOnlyGuard {
868 state_manager: Arc::clone(&self.context.state_manager),
869 speaker_id: self.context.coordinator_id.clone(),
870 property_key: P::KEY,
871 }),
872 )
873 }
874 }
875 } else {
876 self.context
877 .state_manager
878 .register_watch(&self.context.coordinator_id, P::KEY);
879 (
880 WatchMode::CacheOnly,
881 WatchCleanup::CacheOnly(CacheOnlyGuard {
882 state_manager: Arc::clone(&self.context.state_manager),
883 speaker_id: self.context.coordinator_id.clone(),
884 property_key: P::KEY,
885 }),
886 )
887 };
888
889 Ok(WatchHandle {
890 value: self.get(),
891 mode,
892 _cleanup: cleanup,
893 })
894 }
895
896 #[must_use = "returns whether the property is being watched"]
898 pub fn is_watched(&self) -> bool {
899 self.context
900 .state_manager
901 .is_watched(&self.context.coordinator_id, P::KEY)
902 }
903
904 pub fn group_id(&self) -> &GroupId {
906 &self.context.group_id
907 }
908}
909
910pub trait GroupFetchable: SonosProperty {
912 type Operation: UPnPOperation;
914
915 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
917
918 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
920}
921
922impl<P: GroupFetchable> GroupPropertyHandle<P> {
923 #[must_use = "returns the fetched value from the device"]
925 pub fn fetch(&self) -> Result<P, SdkError> {
926 let operation = P::build_operation()?;
927
928 let response = self
929 .context
930 .api_client
931 .execute_enhanced(&self.context.coordinator_ip.to_string(), operation)
932 .map_err(SdkError::ApiError)?;
933
934 let property_value = P::from_response(response);
935
936 self.context
937 .state_manager
938 .set_group_property(&self.context.group_id, property_value.clone());
939
940 Ok(property_value)
941 }
942}
943
944impl GroupFetchable for GroupVolume {
949 type Operation = GetGroupVolumeOperation;
950
951 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
952 group_rendering_control::get_group_volume()
953 .build()
954 .map_err(|e| build_error("GetGroupVolume", e))
955 }
956
957 fn from_response(response: GetGroupVolumeResponse) -> Self {
958 GroupVolume::new(response.current_volume)
959 }
960}
961
962impl GroupFetchable for GroupMute {
963 type Operation = GetGroupMuteOperation;
964
965 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
966 group_rendering_control::get_group_mute()
967 .build()
968 .map_err(|e| build_error("GetGroupMute", e))
969 }
970
971 fn from_response(response: GetGroupMuteResponse) -> Self {
972 GroupMute::new(response.current_mute)
973 }
974}
975
976pub type GroupVolumeHandle = GroupPropertyHandle<GroupVolume>;
982
983pub type GroupMuteHandle = GroupPropertyHandle<GroupMute>;
985
986pub type GroupVolumeChangeableHandle = GroupPropertyHandle<GroupVolumeChangeable>;
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992 use sonos_discovery::Device;
993
994 fn create_test_state_manager() -> Arc<StateManager> {
995 let manager = StateManager::new().unwrap();
996 let devices = vec![Device {
997 id: "RINCON_TEST123".to_string(),
998 name: "Test Speaker".to_string(),
999 room_name: "Test Room".to_string(),
1000 ip_address: "192.168.1.100".to_string(),
1001 port: 1400,
1002 model_name: "Sonos One".to_string(),
1003 }];
1004 manager.add_devices(devices).unwrap();
1005 Arc::new(manager)
1006 }
1007
1008 fn create_test_context(state_manager: Arc<StateManager>) -> Arc<SpeakerContext> {
1009 SpeakerContext::new(
1010 SpeakerId::new("RINCON_TEST123"),
1011 "192.168.1.100".parse().unwrap(),
1012 state_manager,
1013 SonosClient::new(),
1014 )
1015 }
1016
1017 #[test]
1018 fn test_property_handle_creation() {
1019 let state_manager = create_test_state_manager();
1020 let context = create_test_context(state_manager);
1021 let speaker_ip: IpAddr = "192.168.1.100".parse().unwrap();
1022
1023 let handle: VolumeHandle = PropertyHandle::new(context);
1024
1025 assert_eq!(handle.speaker_id().as_str(), "RINCON_TEST123");
1026 assert_eq!(handle.speaker_ip(), speaker_ip);
1027 }
1028
1029 #[test]
1030 fn test_get_returns_none_initially() {
1031 let state_manager = create_test_state_manager();
1032 let context = create_test_context(state_manager);
1033
1034 let handle: VolumeHandle = PropertyHandle::new(context);
1035
1036 assert!(handle.get().is_none());
1037 }
1038
1039 #[test]
1040 fn test_get_returns_cached_value() {
1041 let state_manager = create_test_state_manager();
1042 let speaker_id = SpeakerId::new("RINCON_TEST123");
1043
1044 state_manager.set_property(&speaker_id, Volume::new(75));
1045
1046 let context = create_test_context(Arc::clone(&state_manager));
1047 let handle: VolumeHandle = PropertyHandle::new(context);
1048
1049 assert_eq!(handle.get(), Some(Volume::new(75)));
1050 }
1051
1052 #[test]
1053 fn test_watch_registers_property() {
1054 let state_manager = create_test_state_manager();
1055 let context = create_test_context(Arc::clone(&state_manager));
1056
1057 let handle: VolumeHandle = PropertyHandle::new(context);
1058
1059 assert!(!handle.is_watched());
1060 let _wh = handle.watch().unwrap();
1061 assert!(handle.is_watched());
1062 }
1063
1064 #[test]
1065 fn test_drop_watch_handle_unregisters_property() {
1066 let state_manager = create_test_state_manager();
1067 let context = create_test_context(Arc::clone(&state_manager));
1068
1069 let handle: VolumeHandle = PropertyHandle::new(context);
1070
1071 let wh = handle.watch().unwrap();
1072 assert!(handle.is_watched());
1073
1074 drop(wh);
1075 assert!(!handle.is_watched());
1076 }
1077
1078 #[test]
1079 fn test_watch_returns_current_value() {
1080 let state_manager = create_test_state_manager();
1081 let speaker_id = SpeakerId::new("RINCON_TEST123");
1082
1083 state_manager.set_property(&speaker_id, Volume::new(50));
1084
1085 let context = create_test_context(Arc::clone(&state_manager));
1086 let handle: VolumeHandle = PropertyHandle::new(context);
1087
1088 let wh = handle.watch().unwrap();
1089 assert_eq!(*wh, Some(Volume::new(50)));
1090 assert_eq!(wh.value(), Some(&Volume::new(50)));
1091 assert_eq!(wh.mode(), WatchMode::CacheOnly);
1093 }
1094
1095 #[test]
1096 fn test_watch_handle_deref() {
1097 let state_manager = create_test_state_manager();
1098 let speaker_id = SpeakerId::new("RINCON_TEST123");
1099
1100 state_manager.set_property(&speaker_id, Volume::new(75));
1101
1102 let context = create_test_context(Arc::clone(&state_manager));
1103 let handle: VolumeHandle = PropertyHandle::new(context);
1104
1105 let wh = handle.watch().unwrap();
1106 assert!(wh.has_value());
1108 assert!(!wh.has_realtime_events());
1109 if let Some(v) = &*wh {
1110 assert_eq!(v.value(), 75);
1111 } else {
1112 panic!("Expected Some value");
1113 }
1114 }
1115
1116 #[test]
1117 fn test_property_handle_clone() {
1118 let state_manager = create_test_state_manager();
1119 let speaker_id = SpeakerId::new("RINCON_TEST123");
1120
1121 state_manager.set_property(&speaker_id, Volume::new(60));
1122
1123 let context = create_test_context(Arc::clone(&state_manager));
1124 let handle: VolumeHandle = PropertyHandle::new(context);
1125
1126 let cloned = handle.clone();
1127
1128 assert_eq!(handle.get(), cloned.get());
1129 assert_eq!(handle.get(), Some(Volume::new(60)));
1130 }
1131
1132 fn create_test_group_context(state_manager: Arc<StateManager>) -> Arc<GroupContext> {
1137 GroupContext::new(
1138 GroupId::new("RINCON_TEST123:1"),
1139 SpeakerId::new("RINCON_TEST123"),
1140 "192.168.1.100".parse().unwrap(),
1141 state_manager,
1142 SonosClient::new(),
1143 )
1144 }
1145
1146 #[test]
1147 fn test_group_property_handle_get_returns_none_initially() {
1148 let state_manager = create_test_state_manager();
1149 let context = create_test_group_context(state_manager);
1150
1151 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1152
1153 assert!(handle.get().is_none());
1154 }
1155
1156 #[test]
1157 fn test_group_property_handle_get_returns_cached_value() {
1158 let state_manager = create_test_state_manager();
1159 let group_id = GroupId::new("RINCON_TEST123:1");
1160
1161 state_manager.set_group_property(&group_id, GroupVolume::new(65));
1163
1164 let context = create_test_group_context(Arc::clone(&state_manager));
1165 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1166
1167 assert_eq!(handle.get(), Some(GroupVolume::new(65)));
1168 }
1169
1170 #[test]
1171 fn test_group_property_handle_watch_and_drop() {
1172 let state_manager = create_test_state_manager();
1173 let context = create_test_group_context(Arc::clone(&state_manager));
1174
1175 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1176
1177 assert!(!handle.is_watched());
1178 let wh = handle.watch().unwrap();
1179 assert!(handle.is_watched());
1180
1181 drop(wh);
1182 assert!(!handle.is_watched());
1183 }
1184
1185 #[test]
1186 fn test_group_property_handle_group_id() {
1187 let state_manager = create_test_state_manager();
1188 let context = create_test_group_context(state_manager);
1189
1190 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1191
1192 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1193 }
1194
1195 #[test]
1196 fn test_group_mute_handle_accessible() {
1197 let state_manager = create_test_state_manager();
1198 let context = create_test_group_context(state_manager);
1199
1200 let handle: GroupMuteHandle = GroupPropertyHandle::new(context);
1201
1202 assert!(handle.get().is_none());
1203 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1204 }
1205
1206 #[test]
1207 fn test_group_volume_changeable_handle_accessible() {
1208 let state_manager = create_test_state_manager();
1209 let context = create_test_group_context(state_manager);
1210
1211 let handle: GroupVolumeChangeableHandle = GroupPropertyHandle::new(context);
1212
1213 assert!(handle.get().is_none());
1214 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1215 }
1216
1217 #[test]
1222 fn test_fetchable_impls_exist() {
1223 fn assert_fetchable<T: Fetchable>() {}
1224 assert_fetchable::<Volume>();
1225 assert_fetchable::<PlaybackState>();
1226 assert_fetchable::<Position>();
1227 assert_fetchable::<Mute>();
1228 assert_fetchable::<Bass>();
1229 assert_fetchable::<Treble>();
1230 assert_fetchable::<Loudness>();
1231 assert_fetchable::<CurrentTrack>();
1232 }
1233
1234 #[test]
1235 fn test_fetchable_with_context_impls_exist() {
1236 fn assert_fetchable_with_context<T: FetchableWithContext>() {}
1237 assert_fetchable_with_context::<GroupMembership>();
1238 }
1239
1240 #[test]
1241 fn test_group_fetchable_impls_exist() {
1242 fn assert_group_fetchable<T: GroupFetchable>() {}
1243 assert_group_fetchable::<GroupVolume>();
1244 assert_group_fetchable::<GroupMute>();
1245 }
1246}