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
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)]
170enum WatchCleanup {
171 Guard(WatchGuard),
172 CacheOnly(CacheOnlyGuard),
173}
174
175struct CacheOnlyGuard {
178 state_manager: Arc<StateManager>,
179 speaker_id: SpeakerId,
180 property_key: &'static str,
181}
182
183impl Drop for CacheOnlyGuard {
184 fn drop(&mut self) {
185 self.state_manager
186 .unregister_watch(&self.speaker_id, self.property_key);
187 }
188}
189
190pub trait Fetchable: SonosProperty {
217 type Operation: UPnPOperation;
219
220 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
222
223 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
225}
226
227pub trait FetchableWithContext: SonosProperty {
232 type Operation: UPnPOperation;
234
235 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
237
238 fn from_response_with_context(
240 response: <Self::Operation as UPnPOperation>::Response,
241 speaker_id: &SpeakerId,
242 ) -> Option<Self>;
243}
244
245#[derive(Clone)]
270pub struct PropertyHandle<P: SonosProperty> {
271 context: Arc<SpeakerContext>,
272 _phantom: PhantomData<P>,
273}
274
275impl<P: SonosProperty> PropertyHandle<P> {
276 pub fn new(context: Arc<SpeakerContext>) -> Self {
278 Self {
279 context,
280 _phantom: PhantomData,
281 }
282 }
283
284 #[must_use = "returns the cached property value"]
297 pub fn get(&self) -> Option<P> {
298 self.context
299 .state_manager
300 .get_property::<P>(&self.context.speaker_id)
301 }
302
303 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
328 tracing::trace!(
329 "watch() called for {:?} on {}",
330 P::SERVICE,
331 self.context.speaker_id.as_str()
332 );
333
334 if self.context.state_manager.event_manager().is_none() {
336 if let Some(init) = self.context.state_manager.event_init() {
337 tracing::debug!(
338 "Event manager not initialized, triggering lazy init for {:?} on {}",
339 P::SERVICE,
340 self.context.speaker_id.as_str()
341 );
342 init().map_err(|e| SdkError::EventManager(e.to_string()))?;
343 } else {
344 tracing::debug!(
345 "No event_init closure available (test mode?) for {}",
346 self.context.speaker_id.as_str()
347 );
348 }
349 }
350
351 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
352 match em.acquire_watch(
353 &self.context.speaker_id,
354 P::KEY,
355 self.context.speaker_ip,
356 P::SERVICE,
357 ) {
358 Ok(guard) => (WatchMode::Events, WatchCleanup::Guard(guard)),
359 Err(e) => {
360 tracing::warn!(
361 "Failed to subscribe to {:?} for {}: {} - falling back to polling",
362 P::SERVICE,
363 self.context.speaker_id.as_str(),
364 e
365 );
366 self.context
368 .state_manager
369 .register_watch(&self.context.speaker_id, P::KEY);
370 (
371 WatchMode::Polling,
372 WatchCleanup::CacheOnly(CacheOnlyGuard {
373 state_manager: Arc::clone(&self.context.state_manager),
374 speaker_id: self.context.speaker_id.clone(),
375 property_key: P::KEY,
376 }),
377 )
378 }
379 }
380 } else {
381 tracing::warn!(
383 "No event manager available for {} — falling back to cache-only mode",
384 self.context.speaker_id.as_str()
385 );
386 self.context
387 .state_manager
388 .register_watch(&self.context.speaker_id, P::KEY);
389 (
390 WatchMode::CacheOnly,
391 WatchCleanup::CacheOnly(CacheOnlyGuard {
392 state_manager: Arc::clone(&self.context.state_manager),
393 speaker_id: self.context.speaker_id.clone(),
394 property_key: P::KEY,
395 }),
396 )
397 };
398
399 tracing::debug!(
400 "watch() resolved to {:?} for {} on {}",
401 mode,
402 P::KEY,
403 self.context.speaker_id.as_str()
404 );
405
406 Ok(WatchHandle {
407 value: self.get(),
408 mode,
409 _cleanup: cleanup,
410 })
411 }
412
413 #[must_use = "returns whether the property is being watched"]
428 pub fn is_watched(&self) -> bool {
429 self.context
430 .state_manager
431 .is_watched(&self.context.speaker_id, P::KEY)
432 }
433
434 pub fn speaker_id(&self) -> &SpeakerId {
436 &self.context.speaker_id
437 }
438
439 pub fn speaker_ip(&self) -> IpAddr {
441 self.context.speaker_ip
442 }
443}
444
445impl<P: Fetchable> PropertyHandle<P> {
450 #[must_use = "returns the fetched value from the device"]
466 pub fn fetch(&self) -> Result<P, SdkError> {
467 let operation = P::build_operation()?;
469
470 let response = self
472 .context
473 .api_client
474 .execute_enhanced(&self.context.speaker_ip.to_string(), operation)
475 .map_err(SdkError::ApiError)?;
476
477 let property_value = P::from_response(response);
479
480 self.context
482 .state_manager
483 .set_property(&self.context.speaker_id, property_value.clone());
484
485 Ok(property_value)
486 }
487}
488
489impl PropertyHandle<GroupMembership> {
498 #[must_use = "returns the fetched value from the device"]
503 pub fn fetch(&self) -> Result<GroupMembership, SdkError> {
504 let operation = <GroupMembership as FetchableWithContext>::build_operation()?;
505
506 let response = self
507 .context
508 .api_client
509 .execute_enhanced(&self.context.speaker_ip.to_string(), operation)
510 .map_err(SdkError::ApiError)?;
511
512 let property_value =
513 GroupMembership::from_response_with_context(response, &self.context.speaker_id)
514 .ok_or_else(|| {
515 SdkError::FetchFailed(format!(
516 "Speaker {} not found in topology response",
517 self.context.speaker_id.as_str()
518 ))
519 })?;
520
521 self.context
522 .state_manager
523 .set_property(&self.context.speaker_id, property_value.clone());
524
525 Ok(property_value)
526 }
527}
528
529use sonos_api::services::{
534 av_transport::{
535 self, GetPositionInfoOperation, GetPositionInfoResponse, GetTransportInfoOperation,
536 GetTransportInfoResponse,
537 },
538 group_rendering_control::{
539 self, GetGroupMuteOperation, GetGroupMuteResponse, GetGroupVolumeOperation,
540 GetGroupVolumeResponse,
541 },
542 rendering_control::{
543 self, GetBassOperation, GetBassResponse, GetLoudnessOperation, GetLoudnessResponse,
544 GetMuteOperation, GetMuteResponse, GetTrebleOperation, GetTrebleResponse,
545 GetVolumeOperation, GetVolumeResponse,
546 },
547 zone_group_topology::{self, GetZoneGroupStateOperation, GetZoneGroupStateResponse},
548};
549use sonos_state::{
550 Bass, CurrentTrack, GroupId, GroupMembership, GroupMute, GroupVolume, GroupVolumeChangeable,
551 Loudness, Mute, PlaybackState, Position, Treble, Volume,
552};
553
554fn build_error<E: std::fmt::Display>(operation_name: &str, e: E) -> SdkError {
560 SdkError::FetchFailed(format!("Failed to build {operation_name} operation: {e}"))
561}
562
563impl Fetchable for Volume {
568 type Operation = GetVolumeOperation;
569
570 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
571 rendering_control::get_volume_operation("Master".to_string())
572 .build()
573 .map_err(|e| build_error("GetVolume", e))
574 }
575
576 fn from_response(response: GetVolumeResponse) -> Self {
577 Volume::new(response.current_volume)
578 }
579}
580
581impl Fetchable for PlaybackState {
582 type Operation = GetTransportInfoOperation;
583
584 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
585 av_transport::get_transport_info_operation()
586 .build()
587 .map_err(|e| build_error("GetTransportInfo", e))
588 }
589
590 fn from_response(response: GetTransportInfoResponse) -> Self {
591 match response.current_transport_state.as_str() {
592 "PLAYING" => PlaybackState::Playing,
593 "PAUSED" | "PAUSED_PLAYBACK" => PlaybackState::Paused,
594 "STOPPED" => PlaybackState::Stopped,
595 _ => PlaybackState::Transitioning,
596 }
597 }
598}
599
600impl Fetchable for Position {
601 type Operation = GetPositionInfoOperation;
602
603 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
604 av_transport::get_position_info_operation()
605 .build()
606 .map_err(|e| build_error("GetPositionInfo", e))
607 }
608
609 fn from_response(response: GetPositionInfoResponse) -> Self {
610 let position_ms = Position::parse_time_to_ms(&response.rel_time).unwrap_or(0);
611 let duration_ms = Position::parse_time_to_ms(&response.track_duration).unwrap_or(0);
612 Position::new(position_ms, duration_ms)
613 }
614}
615
616impl Fetchable for Mute {
617 type Operation = GetMuteOperation;
618
619 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
620 rendering_control::get_mute_operation("Master".to_string())
621 .build()
622 .map_err(|e| build_error("GetMute", e))
623 }
624
625 fn from_response(response: GetMuteResponse) -> Self {
626 Mute::new(response.current_mute)
627 }
628}
629
630impl Fetchable for Bass {
631 type Operation = GetBassOperation;
632
633 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
634 rendering_control::get_bass_operation()
635 .build()
636 .map_err(|e| build_error("GetBass", e))
637 }
638
639 fn from_response(response: GetBassResponse) -> Self {
640 Bass::new(response.current_bass)
641 }
642}
643
644impl Fetchable for Treble {
645 type Operation = GetTrebleOperation;
646
647 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
648 rendering_control::get_treble_operation()
649 .build()
650 .map_err(|e| build_error("GetTreble", e))
651 }
652
653 fn from_response(response: GetTrebleResponse) -> Self {
654 Treble::new(response.current_treble)
655 }
656}
657
658impl Fetchable for Loudness {
659 type Operation = GetLoudnessOperation;
660
661 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
662 rendering_control::get_loudness_operation("Master".to_string())
663 .build()
664 .map_err(|e| build_error("GetLoudness", e))
665 }
666
667 fn from_response(response: GetLoudnessResponse) -> Self {
668 Loudness::new(response.current_loudness)
669 }
670}
671
672impl Fetchable for CurrentTrack {
673 type Operation = GetPositionInfoOperation;
674
675 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
676 av_transport::get_position_info_operation()
677 .build()
678 .map_err(|e| build_error("GetPositionInfo", e))
679 }
680
681 fn from_response(response: GetPositionInfoResponse) -> Self {
682 let metadata = if response.track_meta_data.is_empty()
683 || response.track_meta_data == "NOT_IMPLEMENTED"
684 {
685 None
686 } else {
687 Some(response.track_meta_data.as_str())
688 };
689 let (title, artist, album, album_art_uri) = sonos_state::parse_track_metadata(metadata);
690 CurrentTrack {
691 title,
692 artist,
693 album,
694 album_art_uri,
695 uri: Some(response.track_uri).filter(|s| !s.is_empty()),
696 }
697 }
698}
699
700impl FetchableWithContext for GroupMembership {
705 type Operation = GetZoneGroupStateOperation;
706
707 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
708 zone_group_topology::get_zone_group_state_operation()
709 .build()
710 .map_err(|e| build_error("GetZoneGroupState", e))
711 }
712
713 fn from_response_with_context(
714 response: GetZoneGroupStateResponse,
715 speaker_id: &SpeakerId,
716 ) -> Option<Self> {
717 let zone_groups =
718 zone_group_topology::parse_zone_group_state_xml(&response.zone_group_state).ok()?;
719
720 for group in &zone_groups {
721 let is_member = group.members.iter().any(|m| m.uuid == speaker_id.as_str());
722 if is_member {
723 let is_coordinator = group.coordinator == speaker_id.as_str();
724 return Some(GroupMembership::new(
725 GroupId::new(&group.id),
726 is_coordinator,
727 ));
728 }
729 }
730
731 None
732 }
733}
734
735pub type VolumeHandle = PropertyHandle<Volume>;
752
753pub type PlaybackStateHandle = PropertyHandle<PlaybackState>;
755
756pub type MuteHandle = PropertyHandle<Mute>;
758
759pub type BassHandle = PropertyHandle<Bass>;
761
762pub type TrebleHandle = PropertyHandle<Treble>;
764
765pub type LoudnessHandle = PropertyHandle<Loudness>;
767
768pub type PositionHandle = PropertyHandle<Position>;
770
771pub type CurrentTrackHandle = PropertyHandle<CurrentTrack>;
773
774pub type GroupMembershipHandle = PropertyHandle<GroupMembership>;
776
777#[derive(Clone)]
786pub struct GroupContext {
787 pub(crate) group_id: GroupId,
788 pub(crate) coordinator_id: SpeakerId,
789 pub(crate) coordinator_ip: IpAddr,
790 pub(crate) state_manager: Arc<StateManager>,
791 pub(crate) api_client: SonosClient,
792}
793
794impl GroupContext {
795 pub fn new(
797 group_id: GroupId,
798 coordinator_id: SpeakerId,
799 coordinator_ip: IpAddr,
800 state_manager: Arc<StateManager>,
801 api_client: SonosClient,
802 ) -> Arc<Self> {
803 Arc::new(Self {
804 group_id,
805 coordinator_id,
806 coordinator_ip,
807 state_manager,
808 api_client,
809 })
810 }
811}
812
813#[derive(Clone)]
819pub struct GroupPropertyHandle<P: SonosProperty> {
820 context: Arc<GroupContext>,
821 _phantom: PhantomData<P>,
822}
823
824impl<P: SonosProperty> GroupPropertyHandle<P> {
825 pub fn new(context: Arc<GroupContext>) -> Self {
827 Self {
828 context,
829 _phantom: PhantomData,
830 }
831 }
832
833 #[must_use = "returns the cached property value"]
835 pub fn get(&self) -> Option<P> {
836 self.context
837 .state_manager
838 .get_group_property::<P>(&self.context.group_id)
839 }
840
841 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
846 if self.context.state_manager.event_manager().is_none() {
848 if let Some(init) = self.context.state_manager.event_init() {
849 tracing::debug!(
850 "Event manager not initialized, triggering lazy init for group {:?} on {}",
851 P::SERVICE,
852 self.context.group_id.as_str()
853 );
854 init().map_err(|e| SdkError::EventManager(e.to_string()))?;
855 } else {
856 tracing::debug!(
857 "No event_init closure available (test mode?) for group {}",
858 self.context.group_id.as_str()
859 );
860 }
861 }
862
863 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
864 match em.acquire_watch(
865 &self.context.coordinator_id,
866 P::KEY,
867 self.context.coordinator_ip,
868 P::SERVICE,
869 ) {
870 Ok(guard) => (WatchMode::Events, WatchCleanup::Guard(guard)),
871 Err(e) => {
872 tracing::warn!(
873 "Failed to subscribe to {:?} for group {}: {} - falling back to polling",
874 P::SERVICE,
875 self.context.group_id.as_str(),
876 e
877 );
878 self.context
879 .state_manager
880 .register_watch(&self.context.coordinator_id, P::KEY);
881 (
882 WatchMode::Polling,
883 WatchCleanup::CacheOnly(CacheOnlyGuard {
884 state_manager: Arc::clone(&self.context.state_manager),
885 speaker_id: self.context.coordinator_id.clone(),
886 property_key: P::KEY,
887 }),
888 )
889 }
890 }
891 } else {
892 self.context
893 .state_manager
894 .register_watch(&self.context.coordinator_id, P::KEY);
895 (
896 WatchMode::CacheOnly,
897 WatchCleanup::CacheOnly(CacheOnlyGuard {
898 state_manager: Arc::clone(&self.context.state_manager),
899 speaker_id: self.context.coordinator_id.clone(),
900 property_key: P::KEY,
901 }),
902 )
903 };
904
905 Ok(WatchHandle {
906 value: self.get(),
907 mode,
908 _cleanup: cleanup,
909 })
910 }
911
912 #[must_use = "returns whether the property is being watched"]
914 pub fn is_watched(&self) -> bool {
915 self.context
916 .state_manager
917 .is_watched(&self.context.coordinator_id, P::KEY)
918 }
919
920 pub fn group_id(&self) -> &GroupId {
922 &self.context.group_id
923 }
924}
925
926pub trait GroupFetchable: SonosProperty {
928 type Operation: UPnPOperation;
930
931 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
933
934 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
936}
937
938impl<P: GroupFetchable> GroupPropertyHandle<P> {
939 #[must_use = "returns the fetched value from the device"]
941 pub fn fetch(&self) -> Result<P, SdkError> {
942 let operation = P::build_operation()?;
943
944 let response = self
945 .context
946 .api_client
947 .execute_enhanced(&self.context.coordinator_ip.to_string(), operation)
948 .map_err(SdkError::ApiError)?;
949
950 let property_value = P::from_response(response);
951
952 self.context
953 .state_manager
954 .set_group_property(&self.context.group_id, property_value.clone());
955
956 Ok(property_value)
957 }
958}
959
960impl GroupFetchable for GroupVolume {
965 type Operation = GetGroupVolumeOperation;
966
967 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
968 group_rendering_control::get_group_volume()
969 .build()
970 .map_err(|e| build_error("GetGroupVolume", e))
971 }
972
973 fn from_response(response: GetGroupVolumeResponse) -> Self {
974 GroupVolume::new(response.current_volume)
975 }
976}
977
978impl GroupFetchable for GroupMute {
979 type Operation = GetGroupMuteOperation;
980
981 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
982 group_rendering_control::get_group_mute()
983 .build()
984 .map_err(|e| build_error("GetGroupMute", e))
985 }
986
987 fn from_response(response: GetGroupMuteResponse) -> Self {
988 GroupMute::new(response.current_mute)
989 }
990}
991
992pub type GroupVolumeHandle = GroupPropertyHandle<GroupVolume>;
998
999pub type GroupMuteHandle = GroupPropertyHandle<GroupMute>;
1001
1002pub type GroupVolumeChangeableHandle = GroupPropertyHandle<GroupVolumeChangeable>;
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use sonos_discovery::Device;
1009
1010 fn create_test_state_manager() -> Arc<StateManager> {
1011 let manager = StateManager::new().unwrap();
1012 let devices = vec![Device {
1013 id: "RINCON_TEST123".to_string(),
1014 name: "Test Speaker".to_string(),
1015 room_name: "Test Room".to_string(),
1016 ip_address: "192.168.1.100".to_string(),
1017 port: 1400,
1018 model_name: "Sonos One".to_string(),
1019 }];
1020 manager.add_devices(devices).unwrap();
1021 Arc::new(manager)
1022 }
1023
1024 fn create_test_context(state_manager: Arc<StateManager>) -> Arc<SpeakerContext> {
1025 SpeakerContext::new(
1026 SpeakerId::new("RINCON_TEST123"),
1027 "192.168.1.100".parse().unwrap(),
1028 state_manager,
1029 SonosClient::new(),
1030 )
1031 }
1032
1033 #[test]
1034 fn test_property_handle_creation() {
1035 let state_manager = create_test_state_manager();
1036 let context = create_test_context(state_manager);
1037 let speaker_ip: IpAddr = "192.168.1.100".parse().unwrap();
1038
1039 let handle: VolumeHandle = PropertyHandle::new(context);
1040
1041 assert_eq!(handle.speaker_id().as_str(), "RINCON_TEST123");
1042 assert_eq!(handle.speaker_ip(), speaker_ip);
1043 }
1044
1045 #[test]
1046 fn test_get_returns_none_initially() {
1047 let state_manager = create_test_state_manager();
1048 let context = create_test_context(state_manager);
1049
1050 let handle: VolumeHandle = PropertyHandle::new(context);
1051
1052 assert!(handle.get().is_none());
1053 }
1054
1055 #[test]
1056 fn test_get_returns_cached_value() {
1057 let state_manager = create_test_state_manager();
1058 let speaker_id = SpeakerId::new("RINCON_TEST123");
1059
1060 state_manager.set_property(&speaker_id, Volume::new(75));
1061
1062 let context = create_test_context(Arc::clone(&state_manager));
1063 let handle: VolumeHandle = PropertyHandle::new(context);
1064
1065 assert_eq!(handle.get(), Some(Volume::new(75)));
1066 }
1067
1068 #[test]
1069 fn test_watch_registers_property() {
1070 let state_manager = create_test_state_manager();
1071 let context = create_test_context(Arc::clone(&state_manager));
1072
1073 let handle: VolumeHandle = PropertyHandle::new(context);
1074
1075 assert!(!handle.is_watched());
1076 let _wh = handle.watch().unwrap();
1077 assert!(handle.is_watched());
1078 }
1079
1080 #[test]
1081 fn test_drop_watch_handle_unregisters_property() {
1082 let state_manager = create_test_state_manager();
1083 let context = create_test_context(Arc::clone(&state_manager));
1084
1085 let handle: VolumeHandle = PropertyHandle::new(context);
1086
1087 let wh = handle.watch().unwrap();
1088 assert!(handle.is_watched());
1089
1090 drop(wh);
1091 assert!(!handle.is_watched());
1092 }
1093
1094 #[test]
1095 fn test_watch_returns_current_value() {
1096 let state_manager = create_test_state_manager();
1097 let speaker_id = SpeakerId::new("RINCON_TEST123");
1098
1099 state_manager.set_property(&speaker_id, Volume::new(50));
1100
1101 let context = create_test_context(Arc::clone(&state_manager));
1102 let handle: VolumeHandle = PropertyHandle::new(context);
1103
1104 let wh = handle.watch().unwrap();
1105 assert_eq!(*wh, Some(Volume::new(50)));
1106 assert_eq!(wh.value(), Some(&Volume::new(50)));
1107 assert_eq!(wh.mode(), WatchMode::CacheOnly);
1109 }
1110
1111 #[test]
1112 fn test_watch_handle_deref() {
1113 let state_manager = create_test_state_manager();
1114 let speaker_id = SpeakerId::new("RINCON_TEST123");
1115
1116 state_manager.set_property(&speaker_id, Volume::new(75));
1117
1118 let context = create_test_context(Arc::clone(&state_manager));
1119 let handle: VolumeHandle = PropertyHandle::new(context);
1120
1121 let wh = handle.watch().unwrap();
1122 assert!(wh.has_value());
1124 assert!(!wh.has_realtime_events());
1125 if let Some(v) = &*wh {
1126 assert_eq!(v.value(), 75);
1127 } else {
1128 panic!("Expected Some value");
1129 }
1130 }
1131
1132 #[test]
1133 fn test_property_handle_clone() {
1134 let state_manager = create_test_state_manager();
1135 let speaker_id = SpeakerId::new("RINCON_TEST123");
1136
1137 state_manager.set_property(&speaker_id, Volume::new(60));
1138
1139 let context = create_test_context(Arc::clone(&state_manager));
1140 let handle: VolumeHandle = PropertyHandle::new(context);
1141
1142 let cloned = handle.clone();
1143
1144 assert_eq!(handle.get(), cloned.get());
1145 assert_eq!(handle.get(), Some(Volume::new(60)));
1146 }
1147
1148 fn create_test_group_context(state_manager: Arc<StateManager>) -> Arc<GroupContext> {
1153 GroupContext::new(
1154 GroupId::new("RINCON_TEST123:1"),
1155 SpeakerId::new("RINCON_TEST123"),
1156 "192.168.1.100".parse().unwrap(),
1157 state_manager,
1158 SonosClient::new(),
1159 )
1160 }
1161
1162 #[test]
1163 fn test_group_property_handle_get_returns_none_initially() {
1164 let state_manager = create_test_state_manager();
1165 let context = create_test_group_context(state_manager);
1166
1167 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1168
1169 assert!(handle.get().is_none());
1170 }
1171
1172 #[test]
1173 fn test_group_property_handle_get_returns_cached_value() {
1174 let state_manager = create_test_state_manager();
1175 let group_id = GroupId::new("RINCON_TEST123:1");
1176
1177 state_manager.set_group_property(&group_id, GroupVolume::new(65));
1179
1180 let context = create_test_group_context(Arc::clone(&state_manager));
1181 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1182
1183 assert_eq!(handle.get(), Some(GroupVolume::new(65)));
1184 }
1185
1186 #[test]
1187 fn test_group_property_handle_watch_and_drop() {
1188 let state_manager = create_test_state_manager();
1189 let context = create_test_group_context(Arc::clone(&state_manager));
1190
1191 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1192
1193 assert!(!handle.is_watched());
1194 let wh = handle.watch().unwrap();
1195 assert!(handle.is_watched());
1196
1197 drop(wh);
1198 assert!(!handle.is_watched());
1199 }
1200
1201 #[test]
1202 fn test_group_property_handle_group_id() {
1203 let state_manager = create_test_state_manager();
1204 let context = create_test_group_context(state_manager);
1205
1206 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1207
1208 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1209 }
1210
1211 #[test]
1212 fn test_group_mute_handle_accessible() {
1213 let state_manager = create_test_state_manager();
1214 let context = create_test_group_context(state_manager);
1215
1216 let handle: GroupMuteHandle = GroupPropertyHandle::new(context);
1217
1218 assert!(handle.get().is_none());
1219 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1220 }
1221
1222 #[test]
1223 fn test_group_volume_changeable_handle_accessible() {
1224 let state_manager = create_test_state_manager();
1225 let context = create_test_group_context(state_manager);
1226
1227 let handle: GroupVolumeChangeableHandle = GroupPropertyHandle::new(context);
1228
1229 assert!(handle.get().is_none());
1230 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1231 }
1232
1233 #[test]
1238 fn test_fetchable_impls_exist() {
1239 fn assert_fetchable<T: Fetchable>() {}
1240 assert_fetchable::<Volume>();
1241 assert_fetchable::<PlaybackState>();
1242 assert_fetchable::<Position>();
1243 assert_fetchable::<Mute>();
1244 assert_fetchable::<Bass>();
1245 assert_fetchable::<Treble>();
1246 assert_fetchable::<Loudness>();
1247 assert_fetchable::<CurrentTrack>();
1248 }
1249
1250 #[test]
1251 fn test_fetchable_with_context_impls_exist() {
1252 fn assert_fetchable_with_context<T: FetchableWithContext>() {}
1253 assert_fetchable_with_context::<GroupMembership>();
1254 }
1255
1256 #[test]
1257 fn test_group_fetchable_impls_exist() {
1258 fn assert_group_fetchable<T: GroupFetchable>() {}
1259 assert_group_fetchable::<GroupVolume>();
1260 assert_group_fetchable::<GroupMute>();
1261 }
1262}