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 {
185 state_manager: Arc<StateManager>,
186 speaker_id: SpeakerId,
187 property_key: &'static str,
188}
189
190impl Drop for CacheOnlyGuard {
191 fn drop(&mut self) {
192 self.state_manager
193 .unregister_watch(&self.speaker_id, self.property_key);
194 }
195}
196
197pub trait Fetchable: SonosProperty {
224 type Operation: UPnPOperation;
226
227 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
229
230 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
232}
233
234pub trait FetchableWithContext: SonosProperty {
239 type Operation: UPnPOperation;
241
242 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
244
245 fn from_response_with_context(
247 response: <Self::Operation as UPnPOperation>::Response,
248 speaker_id: &SpeakerId,
249 ) -> Option<Self>;
250}
251
252#[derive(Clone)]
277pub struct PropertyHandle<P: SonosProperty> {
278 context: Arc<SpeakerContext>,
279 _phantom: PhantomData<P>,
280}
281
282impl<P: SonosProperty> PropertyHandle<P> {
283 pub fn new(context: Arc<SpeakerContext>) -> Self {
285 Self {
286 context,
287 _phantom: PhantomData,
288 }
289 }
290
291 #[must_use = "returns the cached property value"]
304 pub fn get(&self) -> Option<P> {
305 self.context
306 .state_manager
307 .get_property::<P>(&self.context.speaker_id)
308 }
309
310 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
335 tracing::trace!(
336 "watch() called for {:?} on {}",
337 P::SERVICE,
338 self.context.speaker_id.as_str()
339 );
340
341 if self.context.state_manager.event_manager().is_none() {
343 if let Some(init) = self.context.state_manager.event_init() {
344 tracing::debug!(
345 "Event manager not initialized, triggering lazy init for {:?} on {}",
346 P::SERVICE,
347 self.context.speaker_id.as_str()
348 );
349 init().map_err(|e| SdkError::EventManager(e.to_string()))?;
350 } else {
351 tracing::debug!(
352 "No event_init closure available (test mode?) for {}",
353 self.context.speaker_id.as_str()
354 );
355 }
356 }
357
358 let (sub_id, sub_ip) = self.context.state_manager.resolve_subscription_target(
360 &self.context.speaker_id,
361 self.context.speaker_ip,
362 P::SERVICE,
363 );
364 let routed_to_coordinator = sub_id != self.context.speaker_id;
365
366 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
367 match em.acquire_watch(&sub_id, P::KEY, sub_ip, P::SERVICE) {
368 Ok(guard) => {
369 if routed_to_coordinator {
370 self.context
372 .state_manager
373 .register_watch(&self.context.speaker_id, P::KEY);
374 (
375 WatchMode::Events,
376 WatchCleanup::CoordinatorGuard {
377 _guard: guard,
378 _member_cleanup: CacheOnlyGuard {
379 state_manager: Arc::clone(&self.context.state_manager),
380 speaker_id: self.context.speaker_id.clone(),
381 property_key: P::KEY,
382 },
383 },
384 )
385 } else {
386 (WatchMode::Events, WatchCleanup::Guard(guard))
387 }
388 }
389 Err(e) => {
390 tracing::warn!(
391 "Failed to subscribe to {:?} for {}: {} - falling back to polling",
392 P::SERVICE,
393 self.context.speaker_id.as_str(),
394 e
395 );
396 self.context
398 .state_manager
399 .register_watch(&self.context.speaker_id, P::KEY);
400 (
401 WatchMode::Polling,
402 WatchCleanup::CacheOnly(CacheOnlyGuard {
403 state_manager: Arc::clone(&self.context.state_manager),
404 speaker_id: self.context.speaker_id.clone(),
405 property_key: P::KEY,
406 }),
407 )
408 }
409 }
410 } else {
411 tracing::warn!(
413 "No event manager available for {} — falling back to cache-only mode",
414 self.context.speaker_id.as_str()
415 );
416 self.context
417 .state_manager
418 .register_watch(&self.context.speaker_id, P::KEY);
419 (
420 WatchMode::CacheOnly,
421 WatchCleanup::CacheOnly(CacheOnlyGuard {
422 state_manager: Arc::clone(&self.context.state_manager),
423 speaker_id: self.context.speaker_id.clone(),
424 property_key: P::KEY,
425 }),
426 )
427 };
428
429 tracing::debug!(
430 "watch() resolved to {:?} for {} on {}",
431 mode,
432 P::KEY,
433 self.context.speaker_id.as_str()
434 );
435
436 Ok(WatchHandle {
437 value: self.get(),
438 mode,
439 _cleanup: cleanup,
440 })
441 }
442
443 #[must_use = "returns whether the property is being watched"]
458 pub fn is_watched(&self) -> bool {
459 self.context
460 .state_manager
461 .is_watched(&self.context.speaker_id, P::KEY)
462 }
463
464 pub fn speaker_id(&self) -> &SpeakerId {
466 &self.context.speaker_id
467 }
468
469 pub fn speaker_ip(&self) -> IpAddr {
471 self.context.speaker_ip
472 }
473}
474
475impl<P: Fetchable> PropertyHandle<P> {
480 pub fn watch_or_fetch(&self) -> Result<WatchHandle<P>, SdkError> {
486 let mut wh = self.watch()?;
487 if wh.value.is_none() {
488 match self.fetch() {
489 Ok(val) => wh.value = Some(val),
490 Err(e) => {
491 tracing::warn!("watch_or_fetch: fetch failed for {}: {e}", P::KEY);
492 }
493 }
494 }
495 Ok(wh)
496 }
497
498 #[must_use = "returns the fetched value from the device"]
514 pub fn fetch(&self) -> Result<P, SdkError> {
515 let operation = P::build_operation()?;
516
517 let (target_id, target_ip) = if P::SERVICE.scope() == ServiceScope::PerCoordinator {
519 self.context.state_manager.resolve_subscription_target(
520 &self.context.speaker_id,
521 self.context.speaker_ip,
522 P::SERVICE,
523 )
524 } else {
525 let current_ip = self
526 .context
527 .state_manager
528 .get_speaker_ip(&self.context.speaker_id)
529 .unwrap_or(self.context.speaker_ip);
530 (self.context.speaker_id.clone(), current_ip)
531 };
532
533 let response = self
534 .context
535 .api_client
536 .execute_enhanced(&target_ip.to_string(), operation)
537 .map_err(SdkError::ApiError)?;
538
539 let property_value = P::from_response(response);
540
541 self.context
543 .state_manager
544 .set_property(&target_id, property_value.clone());
545
546 Ok(property_value)
547 }
548}
549
550impl PropertyHandle<GroupMembership> {
559 #[must_use = "returns the fetched value from the device"]
564 pub fn fetch(&self) -> Result<GroupMembership, SdkError> {
565 let operation = <GroupMembership as FetchableWithContext>::build_operation()?;
566
567 let response = self
568 .context
569 .api_client
570 .execute_enhanced(&self.context.speaker_ip.to_string(), operation)
571 .map_err(SdkError::ApiError)?;
572
573 let property_value =
574 GroupMembership::from_response_with_context(response, &self.context.speaker_id)
575 .ok_or_else(|| {
576 SdkError::FetchFailed(format!(
577 "Speaker {} not found in topology response",
578 self.context.speaker_id.as_str()
579 ))
580 })?;
581
582 self.context
583 .state_manager
584 .set_property(&self.context.speaker_id, property_value.clone());
585
586 Ok(property_value)
587 }
588}
589
590use sonos_api::services::{
595 av_transport::{
596 self, GetPositionInfoOperation, GetPositionInfoResponse, GetTransportInfoOperation,
597 GetTransportInfoResponse,
598 },
599 group_rendering_control::{
600 self, GetGroupMuteOperation, GetGroupMuteResponse, GetGroupVolumeOperation,
601 GetGroupVolumeResponse,
602 },
603 rendering_control::{
604 self, GetBassOperation, GetBassResponse, GetLoudnessOperation, GetLoudnessResponse,
605 GetMuteOperation, GetMuteResponse, GetTrebleOperation, GetTrebleResponse,
606 GetVolumeOperation, GetVolumeResponse,
607 },
608 zone_group_topology::{self, GetZoneGroupStateOperation, GetZoneGroupStateResponse},
609};
610use sonos_state::{
611 Bass, CurrentTrack, GroupId, GroupMembership, GroupMute, GroupVolume, GroupVolumeChangeable,
612 Loudness, Mute, PlaybackState, Position, Treble, Volume,
613};
614
615fn build_error<E: std::fmt::Display>(operation_name: &str, e: E) -> SdkError {
621 SdkError::FetchFailed(format!("Failed to build {operation_name} operation: {e}"))
622}
623
624impl Fetchable for Volume {
629 type Operation = GetVolumeOperation;
630
631 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
632 rendering_control::get_volume_operation("Master".to_string())
633 .build()
634 .map_err(|e| build_error("GetVolume", e))
635 }
636
637 fn from_response(response: GetVolumeResponse) -> Self {
638 Volume::new(response.current_volume)
639 }
640}
641
642impl Fetchable for PlaybackState {
643 type Operation = GetTransportInfoOperation;
644
645 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
646 av_transport::get_transport_info_operation()
647 .build()
648 .map_err(|e| build_error("GetTransportInfo", e))
649 }
650
651 fn from_response(response: GetTransportInfoResponse) -> Self {
652 match response.current_transport_state.as_str() {
653 "PLAYING" => PlaybackState::Playing,
654 "PAUSED" | "PAUSED_PLAYBACK" => PlaybackState::Paused,
655 "STOPPED" => PlaybackState::Stopped,
656 _ => PlaybackState::Transitioning,
657 }
658 }
659}
660
661impl Fetchable for Position {
662 type Operation = GetPositionInfoOperation;
663
664 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
665 av_transport::get_position_info_operation()
666 .build()
667 .map_err(|e| build_error("GetPositionInfo", e))
668 }
669
670 fn from_response(response: GetPositionInfoResponse) -> Self {
671 let position_ms = Position::parse_time_to_ms(&response.rel_time).unwrap_or(0);
672 let duration_ms = Position::parse_time_to_ms(&response.track_duration).unwrap_or(0);
673 Position::new(position_ms, duration_ms)
674 }
675}
676
677impl Fetchable for Mute {
678 type Operation = GetMuteOperation;
679
680 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
681 rendering_control::get_mute_operation("Master".to_string())
682 .build()
683 .map_err(|e| build_error("GetMute", e))
684 }
685
686 fn from_response(response: GetMuteResponse) -> Self {
687 Mute::new(response.current_mute)
688 }
689}
690
691impl Fetchable for Bass {
692 type Operation = GetBassOperation;
693
694 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
695 rendering_control::get_bass_operation()
696 .build()
697 .map_err(|e| build_error("GetBass", e))
698 }
699
700 fn from_response(response: GetBassResponse) -> Self {
701 Bass::new(response.current_bass)
702 }
703}
704
705impl Fetchable for Treble {
706 type Operation = GetTrebleOperation;
707
708 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
709 rendering_control::get_treble_operation()
710 .build()
711 .map_err(|e| build_error("GetTreble", e))
712 }
713
714 fn from_response(response: GetTrebleResponse) -> Self {
715 Treble::new(response.current_treble)
716 }
717}
718
719impl Fetchable for Loudness {
720 type Operation = GetLoudnessOperation;
721
722 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
723 rendering_control::get_loudness_operation("Master".to_string())
724 .build()
725 .map_err(|e| build_error("GetLoudness", e))
726 }
727
728 fn from_response(response: GetLoudnessResponse) -> Self {
729 Loudness::new(response.current_loudness)
730 }
731}
732
733impl Fetchable for CurrentTrack {
734 type Operation = GetPositionInfoOperation;
735
736 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
737 av_transport::get_position_info_operation()
738 .build()
739 .map_err(|e| build_error("GetPositionInfo", e))
740 }
741
742 fn from_response(response: GetPositionInfoResponse) -> Self {
743 let metadata = if response.track_meta_data.is_empty()
744 || response.track_meta_data == "NOT_IMPLEMENTED"
745 {
746 None
747 } else {
748 Some(response.track_meta_data.as_str())
749 };
750 let (title, artist, album, album_art_uri) = sonos_state::parse_track_metadata(metadata);
751 CurrentTrack {
752 title,
753 artist,
754 album,
755 album_art_uri,
756 uri: Some(response.track_uri).filter(|s| !s.is_empty()),
757 }
758 }
759}
760
761impl FetchableWithContext for GroupMembership {
766 type Operation = GetZoneGroupStateOperation;
767
768 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
769 zone_group_topology::get_zone_group_state_operation()
770 .build()
771 .map_err(|e| build_error("GetZoneGroupState", e))
772 }
773
774 fn from_response_with_context(
775 response: GetZoneGroupStateResponse,
776 speaker_id: &SpeakerId,
777 ) -> Option<Self> {
778 let zone_groups =
779 zone_group_topology::parse_zone_group_state_xml(&response.zone_group_state).ok()?;
780
781 for group in &zone_groups {
782 let is_member = group.members.iter().any(|m| m.uuid == speaker_id.as_str());
783 if is_member {
784 let is_coordinator = group.coordinator == speaker_id.as_str();
785 return Some(GroupMembership::new(
786 GroupId::new(&group.id),
787 is_coordinator,
788 ));
789 }
790 }
791
792 None
793 }
794}
795
796pub type VolumeHandle = PropertyHandle<Volume>;
813
814pub type PlaybackStateHandle = PropertyHandle<PlaybackState>;
816
817pub type MuteHandle = PropertyHandle<Mute>;
819
820pub type BassHandle = PropertyHandle<Bass>;
822
823pub type TrebleHandle = PropertyHandle<Treble>;
825
826pub type LoudnessHandle = PropertyHandle<Loudness>;
828
829pub type PositionHandle = PropertyHandle<Position>;
831
832pub type CurrentTrackHandle = PropertyHandle<CurrentTrack>;
834
835pub type GroupMembershipHandle = PropertyHandle<GroupMembership>;
837
838#[derive(Clone)]
847pub struct GroupContext {
848 pub(crate) group_id: GroupId,
849 pub(crate) coordinator_id: SpeakerId,
850 pub(crate) coordinator_ip: IpAddr,
851 pub(crate) state_manager: Arc<StateManager>,
852 pub(crate) api_client: SonosClient,
853}
854
855impl GroupContext {
856 pub fn new(
858 group_id: GroupId,
859 coordinator_id: SpeakerId,
860 coordinator_ip: IpAddr,
861 state_manager: Arc<StateManager>,
862 api_client: SonosClient,
863 ) -> Arc<Self> {
864 Arc::new(Self {
865 group_id,
866 coordinator_id,
867 coordinator_ip,
868 state_manager,
869 api_client,
870 })
871 }
872}
873
874#[derive(Clone)]
880pub struct GroupPropertyHandle<P: SonosProperty> {
881 context: Arc<GroupContext>,
882 _phantom: PhantomData<P>,
883}
884
885impl<P: SonosProperty> GroupPropertyHandle<P> {
886 pub fn new(context: Arc<GroupContext>) -> Self {
888 Self {
889 context,
890 _phantom: PhantomData,
891 }
892 }
893
894 #[must_use = "returns the cached property value"]
896 pub fn get(&self) -> Option<P> {
897 self.context
898 .state_manager
899 .get_group_property::<P>(&self.context.group_id)
900 }
901
902 pub fn watch(&self) -> Result<WatchHandle<P>, SdkError> {
907 if self.context.state_manager.event_manager().is_none() {
909 if let Some(init) = self.context.state_manager.event_init() {
910 tracing::debug!(
911 "Event manager not initialized, triggering lazy init for group {:?} on {}",
912 P::SERVICE,
913 self.context.group_id.as_str()
914 );
915 init().map_err(|e| SdkError::EventManager(e.to_string()))?;
916 } else {
917 tracing::debug!(
918 "No event_init closure available (test mode?) for group {}",
919 self.context.group_id.as_str()
920 );
921 }
922 }
923
924 let (mode, cleanup) = if let Some(em) = self.context.state_manager.event_manager() {
925 match em.acquire_watch(
926 &self.context.coordinator_id,
927 P::KEY,
928 self.context.coordinator_ip,
929 P::SERVICE,
930 ) {
931 Ok(guard) => (WatchMode::Events, WatchCleanup::Guard(guard)),
932 Err(e) => {
933 tracing::warn!(
934 "Failed to subscribe to {:?} for group {}: {} - falling back to polling",
935 P::SERVICE,
936 self.context.group_id.as_str(),
937 e
938 );
939 self.context
940 .state_manager
941 .register_watch(&self.context.coordinator_id, P::KEY);
942 (
943 WatchMode::Polling,
944 WatchCleanup::CacheOnly(CacheOnlyGuard {
945 state_manager: Arc::clone(&self.context.state_manager),
946 speaker_id: self.context.coordinator_id.clone(),
947 property_key: P::KEY,
948 }),
949 )
950 }
951 }
952 } else {
953 self.context
954 .state_manager
955 .register_watch(&self.context.coordinator_id, P::KEY);
956 (
957 WatchMode::CacheOnly,
958 WatchCleanup::CacheOnly(CacheOnlyGuard {
959 state_manager: Arc::clone(&self.context.state_manager),
960 speaker_id: self.context.coordinator_id.clone(),
961 property_key: P::KEY,
962 }),
963 )
964 };
965
966 Ok(WatchHandle {
967 value: self.get(),
968 mode,
969 _cleanup: cleanup,
970 })
971 }
972
973 #[must_use = "returns whether the property is being watched"]
975 pub fn is_watched(&self) -> bool {
976 self.context
977 .state_manager
978 .is_watched(&self.context.coordinator_id, P::KEY)
979 }
980
981 pub fn group_id(&self) -> &GroupId {
983 &self.context.group_id
984 }
985}
986
987pub trait GroupFetchable: SonosProperty {
989 type Operation: UPnPOperation;
991
992 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError>;
994
995 fn from_response(response: <Self::Operation as UPnPOperation>::Response) -> Self;
997}
998
999impl<P: GroupFetchable> GroupPropertyHandle<P> {
1000 pub fn watch_or_fetch(&self) -> Result<WatchHandle<P>, SdkError> {
1003 let mut wh = self.watch()?;
1004 if wh.value.is_none() {
1005 match self.fetch() {
1006 Ok(val) => wh.value = Some(val),
1007 Err(e) => {
1008 tracing::warn!(
1009 "watch_or_fetch: fetch failed for group {} {}: {e}",
1010 self.context.group_id.as_str(),
1011 P::KEY
1012 );
1013 }
1014 }
1015 }
1016 Ok(wh)
1017 }
1018
1019 #[must_use = "returns the fetched value from the device"]
1021 pub fn fetch(&self) -> Result<P, SdkError> {
1022 let operation = P::build_operation()?;
1023
1024 let response = self
1025 .context
1026 .api_client
1027 .execute_enhanced(&self.context.coordinator_ip.to_string(), operation)
1028 .map_err(SdkError::ApiError)?;
1029
1030 let property_value = P::from_response(response);
1031
1032 self.context
1033 .state_manager
1034 .set_group_property(&self.context.group_id, property_value.clone());
1035
1036 Ok(property_value)
1037 }
1038}
1039
1040impl GroupFetchable for GroupVolume {
1045 type Operation = GetGroupVolumeOperation;
1046
1047 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
1048 group_rendering_control::get_group_volume()
1049 .build()
1050 .map_err(|e| build_error("GetGroupVolume", e))
1051 }
1052
1053 fn from_response(response: GetGroupVolumeResponse) -> Self {
1054 GroupVolume::new(response.current_volume)
1055 }
1056}
1057
1058impl GroupFetchable for GroupMute {
1059 type Operation = GetGroupMuteOperation;
1060
1061 fn build_operation() -> Result<ComposableOperation<Self::Operation>, SdkError> {
1062 group_rendering_control::get_group_mute()
1063 .build()
1064 .map_err(|e| build_error("GetGroupMute", e))
1065 }
1066
1067 fn from_response(response: GetGroupMuteResponse) -> Self {
1068 GroupMute::new(response.current_mute)
1069 }
1070}
1071
1072pub type GroupVolumeHandle = GroupPropertyHandle<GroupVolume>;
1078
1079pub type GroupMuteHandle = GroupPropertyHandle<GroupMute>;
1081
1082pub type GroupVolumeChangeableHandle = GroupPropertyHandle<GroupVolumeChangeable>;
1084
1085#[cfg(test)]
1086mod tests {
1087 use super::*;
1088 use sonos_discovery::Device;
1089
1090 fn create_test_state_manager() -> Arc<StateManager> {
1091 let manager = StateManager::new().unwrap();
1092 let devices = vec![Device {
1093 id: "RINCON_TEST123".to_string(),
1094 name: "Test Speaker".to_string(),
1095 room_name: "Test Room".to_string(),
1096 ip_address: "192.168.1.100".to_string(),
1097 port: 1400,
1098 model_name: "Sonos One".to_string(),
1099 }];
1100 manager.add_devices(devices).unwrap();
1101 Arc::new(manager)
1102 }
1103
1104 fn create_test_context(state_manager: Arc<StateManager>) -> Arc<SpeakerContext> {
1105 SpeakerContext::new(
1106 SpeakerId::new("RINCON_TEST123"),
1107 "192.168.1.100".parse().unwrap(),
1108 state_manager,
1109 SonosClient::new(),
1110 )
1111 }
1112
1113 #[test]
1114 fn test_property_handle_creation() {
1115 let state_manager = create_test_state_manager();
1116 let context = create_test_context(state_manager);
1117 let speaker_ip: IpAddr = "192.168.1.100".parse().unwrap();
1118
1119 let handle: VolumeHandle = PropertyHandle::new(context);
1120
1121 assert_eq!(handle.speaker_id().as_str(), "RINCON_TEST123");
1122 assert_eq!(handle.speaker_ip(), speaker_ip);
1123 }
1124
1125 #[test]
1126 fn test_get_returns_none_initially() {
1127 let state_manager = create_test_state_manager();
1128 let context = create_test_context(state_manager);
1129
1130 let handle: VolumeHandle = PropertyHandle::new(context);
1131
1132 assert!(handle.get().is_none());
1133 }
1134
1135 #[test]
1136 fn test_get_returns_cached_value() {
1137 let state_manager = create_test_state_manager();
1138 let speaker_id = SpeakerId::new("RINCON_TEST123");
1139
1140 state_manager.set_property(&speaker_id, Volume::new(75));
1141
1142 let context = create_test_context(Arc::clone(&state_manager));
1143 let handle: VolumeHandle = PropertyHandle::new(context);
1144
1145 assert_eq!(handle.get(), Some(Volume::new(75)));
1146 }
1147
1148 #[test]
1149 fn test_watch_registers_property() {
1150 let state_manager = create_test_state_manager();
1151 let context = create_test_context(Arc::clone(&state_manager));
1152
1153 let handle: VolumeHandle = PropertyHandle::new(context);
1154
1155 assert!(!handle.is_watched());
1156 let _wh = handle.watch().unwrap();
1157 assert!(handle.is_watched());
1158 }
1159
1160 #[test]
1161 fn test_drop_watch_handle_unregisters_property() {
1162 let state_manager = create_test_state_manager();
1163 let context = create_test_context(Arc::clone(&state_manager));
1164
1165 let handle: VolumeHandle = PropertyHandle::new(context);
1166
1167 let wh = handle.watch().unwrap();
1168 assert!(handle.is_watched());
1169
1170 drop(wh);
1171 assert!(!handle.is_watched());
1172 }
1173
1174 #[test]
1175 fn test_watch_returns_current_value() {
1176 let state_manager = create_test_state_manager();
1177 let speaker_id = SpeakerId::new("RINCON_TEST123");
1178
1179 state_manager.set_property(&speaker_id, Volume::new(50));
1180
1181 let context = create_test_context(Arc::clone(&state_manager));
1182 let handle: VolumeHandle = PropertyHandle::new(context);
1183
1184 let wh = handle.watch().unwrap();
1185 assert_eq!(*wh, Some(Volume::new(50)));
1186 assert_eq!(wh.value(), Some(&Volume::new(50)));
1187 assert_eq!(wh.mode(), WatchMode::CacheOnly);
1189 }
1190
1191 #[test]
1192 fn test_watch_handle_deref() {
1193 let state_manager = create_test_state_manager();
1194 let speaker_id = SpeakerId::new("RINCON_TEST123");
1195
1196 state_manager.set_property(&speaker_id, Volume::new(75));
1197
1198 let context = create_test_context(Arc::clone(&state_manager));
1199 let handle: VolumeHandle = PropertyHandle::new(context);
1200
1201 let wh = handle.watch().unwrap();
1202 assert!(wh.has_value());
1204 assert!(!wh.has_realtime_events());
1205 if let Some(v) = &*wh {
1206 assert_eq!(v.value(), 75);
1207 } else {
1208 panic!("Expected Some value");
1209 }
1210 }
1211
1212 #[test]
1213 fn test_property_handle_clone() {
1214 let state_manager = create_test_state_manager();
1215 let speaker_id = SpeakerId::new("RINCON_TEST123");
1216
1217 state_manager.set_property(&speaker_id, Volume::new(60));
1218
1219 let context = create_test_context(Arc::clone(&state_manager));
1220 let handle: VolumeHandle = PropertyHandle::new(context);
1221
1222 let cloned = handle.clone();
1223
1224 assert_eq!(handle.get(), cloned.get());
1225 assert_eq!(handle.get(), Some(Volume::new(60)));
1226 }
1227
1228 fn create_test_group_context(state_manager: Arc<StateManager>) -> Arc<GroupContext> {
1233 GroupContext::new(
1234 GroupId::new("RINCON_TEST123:1"),
1235 SpeakerId::new("RINCON_TEST123"),
1236 "192.168.1.100".parse().unwrap(),
1237 state_manager,
1238 SonosClient::new(),
1239 )
1240 }
1241
1242 #[test]
1243 fn test_group_property_handle_get_returns_none_initially() {
1244 let state_manager = create_test_state_manager();
1245 let context = create_test_group_context(state_manager);
1246
1247 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1248
1249 assert!(handle.get().is_none());
1250 }
1251
1252 #[test]
1253 fn test_group_property_handle_get_returns_cached_value() {
1254 let state_manager = create_test_state_manager();
1255 let group_id = GroupId::new("RINCON_TEST123:1");
1256
1257 state_manager.set_group_property(&group_id, GroupVolume::new(65));
1259
1260 let context = create_test_group_context(Arc::clone(&state_manager));
1261 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1262
1263 assert_eq!(handle.get(), Some(GroupVolume::new(65)));
1264 }
1265
1266 #[test]
1267 fn test_group_property_handle_watch_and_drop() {
1268 let state_manager = create_test_state_manager();
1269 let context = create_test_group_context(Arc::clone(&state_manager));
1270
1271 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1272
1273 assert!(!handle.is_watched());
1274 let wh = handle.watch().unwrap();
1275 assert!(handle.is_watched());
1276
1277 drop(wh);
1278 assert!(!handle.is_watched());
1279 }
1280
1281 #[test]
1282 fn test_group_property_handle_group_id() {
1283 let state_manager = create_test_state_manager();
1284 let context = create_test_group_context(state_manager);
1285
1286 let handle: GroupVolumeHandle = GroupPropertyHandle::new(context);
1287
1288 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1289 }
1290
1291 #[test]
1292 fn test_group_mute_handle_accessible() {
1293 let state_manager = create_test_state_manager();
1294 let context = create_test_group_context(state_manager);
1295
1296 let handle: GroupMuteHandle = GroupPropertyHandle::new(context);
1297
1298 assert!(handle.get().is_none());
1299 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1300 }
1301
1302 #[test]
1303 fn test_group_volume_changeable_handle_accessible() {
1304 let state_manager = create_test_state_manager();
1305 let context = create_test_group_context(state_manager);
1306
1307 let handle: GroupVolumeChangeableHandle = GroupPropertyHandle::new(context);
1308
1309 assert!(handle.get().is_none());
1310 assert_eq!(handle.group_id().as_str(), "RINCON_TEST123:1");
1311 }
1312
1313 #[test]
1318 fn test_fetchable_impls_exist() {
1319 fn assert_fetchable<T: Fetchable>() {}
1320 assert_fetchable::<Volume>();
1321 assert_fetchable::<PlaybackState>();
1322 assert_fetchable::<Position>();
1323 assert_fetchable::<Mute>();
1324 assert_fetchable::<Bass>();
1325 assert_fetchable::<Treble>();
1326 assert_fetchable::<Loudness>();
1327 assert_fetchable::<CurrentTrack>();
1328 }
1329
1330 #[test]
1331 fn test_fetchable_with_context_impls_exist() {
1332 fn assert_fetchable_with_context<T: FetchableWithContext>() {}
1333 assert_fetchable_with_context::<GroupMembership>();
1334 }
1335
1336 #[test]
1337 fn test_group_fetchable_impls_exist() {
1338 fn assert_group_fetchable<T: GroupFetchable>() {}
1339 assert_group_fetchable::<GroupVolume>();
1340 assert_group_fetchable::<GroupMute>();
1341 }
1342}