1use std::net::IpAddr;
14use std::sync::Arc;
15
16use sonos_api::SonosClient;
17use sonos_discovery::Device;
18use sonos_state::{Bass, Loudness, Mute, PlaybackState, SpeakerId, StateManager, Treble, Volume};
19
20use crate::Group;
21
22use sonos_api::operation::{ComposableOperation, UPnPOperation, ValidationError};
23use sonos_api::services::{
24 av_transport::{
25 self, AddURIToQueueResponse, BecomeCoordinatorOfStandaloneGroupResponse,
26 CreateSavedQueueResponse, GetCrossfadeModeResponse, GetCurrentTransportActionsResponse,
27 GetDeviceCapabilitiesResponse, GetMediaInfoResponse,
28 GetRemainingSleepTimerDurationResponse, GetRunningAlarmPropertiesResponse,
29 GetTransportSettingsResponse, RemoveTrackRangeFromQueueResponse, SaveQueueResponse,
30 },
31 rendering_control::{self, SetRelativeVolumeResponse},
32};
33
34use crate::SdkError;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum SeekTarget {
42 Track(u32),
44 Time(String),
46 Delta(String),
48}
49
50impl SeekTarget {
51 fn unit(&self) -> &str {
53 match self {
54 SeekTarget::Track(_) => "TRACK_NR",
55 SeekTarget::Time(_) => "REL_TIME",
56 SeekTarget::Delta(_) => "TIME_DELTA",
57 }
58 }
59
60 fn target(&self) -> String {
62 match self {
63 SeekTarget::Track(n) => n.to_string(),
64 SeekTarget::Time(t) => t.clone(),
65 SeekTarget::Delta(d) => d.clone(),
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum PlayMode {
73 Normal,
75 RepeatAll,
77 RepeatOne,
79 ShuffleNoRepeat,
81 Shuffle,
83 ShuffleRepeatOne,
85}
86
87impl std::fmt::Display for PlayMode {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 PlayMode::Normal => write!(f, "NORMAL"),
91 PlayMode::RepeatAll => write!(f, "REPEAT_ALL"),
92 PlayMode::RepeatOne => write!(f, "REPEAT_ONE"),
93 PlayMode::ShuffleNoRepeat => write!(f, "SHUFFLE_NOREPEAT"),
94 PlayMode::Shuffle => write!(f, "SHUFFLE"),
95 PlayMode::ShuffleRepeatOne => write!(f, "SHUFFLE_REPEAT_ONE"),
96 }
97 }
98}
99
100use crate::property::{
101 BassHandle, CurrentTrackHandle, EventInitFn, GroupMembershipHandle, LoudnessHandle, MuteHandle,
102 PlaybackStateHandle, PositionHandle, PropertyHandle, SpeakerContext, TrebleHandle,
103 VolumeHandle,
104};
105
106#[derive(Clone)]
127pub struct Speaker {
128 pub id: SpeakerId,
130 pub name: String,
132 pub ip: IpAddr,
134 pub model_name: String,
136
137 pub volume: VolumeHandle,
142 pub mute: MuteHandle,
144 pub bass: BassHandle,
146 pub treble: TrebleHandle,
148 pub loudness: LoudnessHandle,
150
151 pub playback_state: PlaybackStateHandle,
156 pub position: PositionHandle,
158 pub current_track: CurrentTrackHandle,
160
161 pub group_membership: GroupMembershipHandle,
166
167 context: Arc<SpeakerContext>,
169}
170
171impl Speaker {
172 pub fn from_device(
188 device: &Device,
189 state_manager: Arc<StateManager>,
190 api_client: SonosClient,
191 ) -> Result<Self, SdkError> {
192 let ip: IpAddr = device
193 .ip_address
194 .parse()
195 .map_err(|_| SdkError::InvalidIpAddress)?;
196
197 let name = if device.room_name.is_empty() || device.room_name == "Unknown" {
198 device.name.clone()
199 } else {
200 device.room_name.clone()
201 };
202
203 Ok(Self::new(
204 SpeakerId::new(&device.id),
205 name,
206 ip,
207 device.model_name.clone(),
208 state_manager,
209 api_client,
210 ))
211 }
212
213 pub fn new(
218 id: SpeakerId,
219 name: String,
220 ip: IpAddr,
221 model_name: String,
222 state_manager: Arc<StateManager>,
223 api_client: SonosClient,
224 ) -> Self {
225 Self::new_with_event_init(id, name, ip, model_name, state_manager, api_client, None)
226 }
227
228 pub(crate) fn new_with_event_init(
233 id: SpeakerId,
234 name: String,
235 ip: IpAddr,
236 model_name: String,
237 state_manager: Arc<StateManager>,
238 api_client: SonosClient,
239 event_init: Option<EventInitFn>,
240 ) -> Self {
241 let context = match event_init {
242 Some(init) => {
243 SpeakerContext::with_event_init(id.clone(), ip, state_manager, api_client, init)
244 }
245 None => SpeakerContext::new(id.clone(), ip, state_manager, api_client),
246 };
247
248 Self {
249 id,
250 name,
251 ip,
252 model_name,
253 volume: PropertyHandle::new(Arc::clone(&context)),
255 mute: PropertyHandle::new(Arc::clone(&context)),
256 bass: PropertyHandle::new(Arc::clone(&context)),
257 treble: PropertyHandle::new(Arc::clone(&context)),
258 loudness: PropertyHandle::new(Arc::clone(&context)),
259 playback_state: PropertyHandle::new(Arc::clone(&context)),
261 position: PropertyHandle::new(Arc::clone(&context)),
262 current_track: PropertyHandle::new(Arc::clone(&context)),
263 group_membership: PropertyHandle::new(Arc::clone(&context)),
265 context,
267 }
268 }
269
270 pub fn group(&self) -> Option<Group> {
288 let info = self
289 .context
290 .state_manager
291 .get_group_for_speaker(&self.context.speaker_id)?;
292 Group::from_info(
293 info,
294 Arc::clone(&self.context.state_manager),
295 self.context.api_client.clone(),
296 )
297 }
298
299 fn exec<Op: UPnPOperation>(
305 &self,
306 operation: Result<ComposableOperation<Op>, ValidationError>,
307 ) -> Result<Op::Response, SdkError> {
308 let op = operation?;
309 self.context
310 .api_client
311 .execute_enhanced(&self.context.speaker_ip.to_string(), op)
312 .map_err(SdkError::ApiError)
313 }
314
315 pub fn play(&self) -> Result<(), SdkError> {
323 self.exec(av_transport::play("1".to_string()).build())?;
324 self.context
325 .state_manager
326 .set_property(&self.context.speaker_id, PlaybackState::Playing);
327 Ok(())
328 }
329
330 pub fn pause(&self) -> Result<(), SdkError> {
334 self.exec(av_transport::pause().build())?;
335 self.context
336 .state_manager
337 .set_property(&self.context.speaker_id, PlaybackState::Paused);
338 Ok(())
339 }
340
341 pub fn stop(&self) -> Result<(), SdkError> {
345 self.exec(av_transport::stop().build())?;
346 self.context
347 .state_manager
348 .set_property(&self.context.speaker_id, PlaybackState::Stopped);
349 Ok(())
350 }
351
352 pub fn next(&self) -> Result<(), SdkError> {
354 self.exec(av_transport::next().build())?;
355 Ok(())
356 }
357
358 pub fn previous(&self) -> Result<(), SdkError> {
360 self.exec(av_transport::previous().build())?;
361 Ok(())
362 }
363
364 pub fn seek(&self, target: SeekTarget) -> Result<(), SdkError> {
378 self.exec(av_transport::seek(target.unit().to_string(), target.target()).build())?;
379 Ok(())
380 }
381
382 pub fn set_av_transport_uri(&self, uri: &str, metadata: &str) -> Result<(), SdkError> {
388 self.exec(
389 av_transport::set_av_transport_uri(uri.to_string(), metadata.to_string()).build(),
390 )?;
391 Ok(())
392 }
393
394 pub fn set_next_av_transport_uri(&self, uri: &str, metadata: &str) -> Result<(), SdkError> {
396 self.exec(
397 av_transport::set_next_av_transport_uri(uri.to_string(), metadata.to_string()).build(),
398 )?;
399 Ok(())
400 }
401
402 pub fn get_media_info(&self) -> Result<GetMediaInfoResponse, SdkError> {
408 self.exec(av_transport::get_media_info().build())
409 }
410
411 pub fn get_transport_settings(&self) -> Result<GetTransportSettingsResponse, SdkError> {
413 self.exec(av_transport::get_transport_settings().build())
414 }
415
416 pub fn get_current_transport_actions(
418 &self,
419 ) -> Result<GetCurrentTransportActionsResponse, SdkError> {
420 self.exec(av_transport::get_current_transport_actions().build())
421 }
422
423 pub fn set_play_mode(&self, mode: PlayMode) -> Result<(), SdkError> {
436 self.exec(av_transport::set_play_mode(mode.to_string()).build())?;
437 Ok(())
438 }
439
440 pub fn get_crossfade_mode(&self) -> Result<GetCrossfadeModeResponse, SdkError> {
442 self.exec(av_transport::get_crossfade_mode().build())
443 }
444
445 pub fn set_crossfade_mode(&self, enabled: bool) -> Result<(), SdkError> {
447 self.exec(av_transport::set_crossfade_mode(enabled).build())?;
448 Ok(())
449 }
450
451 pub fn configure_sleep_timer(&self, duration: &str) -> Result<(), SdkError> {
457 self.exec(av_transport::configure_sleep_timer(duration.to_string()).build())?;
458 Ok(())
459 }
460
461 pub fn cancel_sleep_timer(&self) -> Result<(), SdkError> {
463 self.configure_sleep_timer("")
464 }
465
466 pub fn get_remaining_sleep_timer(
468 &self,
469 ) -> Result<GetRemainingSleepTimerDurationResponse, SdkError> {
470 self.exec(av_transport::get_remaining_sleep_timer_duration().build())
471 }
472
473 pub fn add_uri_to_queue(
479 &self,
480 uri: &str,
481 metadata: &str,
482 position: u32,
483 enqueue_as_next: bool,
484 ) -> Result<AddURIToQueueResponse, SdkError> {
485 self.exec(
486 av_transport::add_uri_to_queue(
487 uri.to_string(),
488 metadata.to_string(),
489 position,
490 enqueue_as_next,
491 )
492 .build(),
493 )
494 }
495
496 pub fn remove_track_from_queue(&self, object_id: &str, update_id: u32) -> Result<(), SdkError> {
498 self.exec(av_transport::remove_track_from_queue(object_id.to_string(), update_id).build())?;
499 Ok(())
500 }
501
502 pub fn remove_all_tracks_from_queue(&self) -> Result<(), SdkError> {
504 self.exec(av_transport::remove_all_tracks_from_queue().build())?;
505 Ok(())
506 }
507
508 pub fn save_queue(&self, title: &str, object_id: &str) -> Result<SaveQueueResponse, SdkError> {
510 self.exec(av_transport::save_queue(title.to_string(), object_id.to_string()).build())
511 }
512
513 pub fn create_saved_queue(
515 &self,
516 title: &str,
517 uri: &str,
518 metadata: &str,
519 ) -> Result<CreateSavedQueueResponse, SdkError> {
520 self.exec(
521 av_transport::create_saved_queue(
522 title.to_string(),
523 uri.to_string(),
524 metadata.to_string(),
525 )
526 .build(),
527 )
528 }
529
530 pub fn remove_track_range_from_queue(
532 &self,
533 update_id: u32,
534 starting_index: u32,
535 number_of_tracks: u32,
536 ) -> Result<RemoveTrackRangeFromQueueResponse, SdkError> {
537 self.exec(
538 av_transport::remove_track_range_from_queue(
539 update_id,
540 starting_index,
541 number_of_tracks,
542 )
543 .build(),
544 )
545 }
546
547 pub fn backup_queue(&self) -> Result<(), SdkError> {
549 self.exec(av_transport::backup_queue().build())?;
550 Ok(())
551 }
552
553 pub fn get_device_capabilities(&self) -> Result<GetDeviceCapabilitiesResponse, SdkError> {
559 self.exec(av_transport::get_device_capabilities().build())
560 }
561
562 pub fn snooze_alarm(&self, duration: &str) -> Result<(), SdkError> {
568 self.exec(av_transport::snooze_alarm(duration.to_string()).build())?;
569 Ok(())
570 }
571
572 pub fn get_running_alarm_properties(
574 &self,
575 ) -> Result<GetRunningAlarmPropertiesResponse, SdkError> {
576 self.exec(av_transport::get_running_alarm_properties().build())
577 }
578
579 pub fn become_standalone(
585 &self,
586 ) -> Result<BecomeCoordinatorOfStandaloneGroupResponse, SdkError> {
587 self.exec(av_transport::become_coordinator_of_standalone_group().build())
588 }
589
590 pub fn delegate_coordination_to(
592 &self,
593 new_coordinator: &SpeakerId,
594 rejoin_group: bool,
595 ) -> Result<(), SdkError> {
596 self.exec(
597 av_transport::delegate_group_coordination_to(
598 new_coordinator.as_str().to_string(),
599 rejoin_group,
600 )
601 .build(),
602 )?;
603 Ok(())
604 }
605
606 pub fn join_group(&self, group: &Group) -> Result<(), SdkError> {
611 group.add_speaker(self)
612 }
613
614 pub fn leave_group(&self) -> Result<BecomeCoordinatorOfStandaloneGroupResponse, SdkError> {
619 self.become_standalone()
620 }
621
622 pub fn set_volume(&self, volume: u8) -> Result<(), SdkError> {
630 self.exec(rendering_control::set_volume("Master".to_string(), volume).build())?;
631 self.context
632 .state_manager
633 .set_property(&self.context.speaker_id, Volume(volume));
634 Ok(())
635 }
636
637 pub fn set_relative_volume(
641 &self,
642 adjustment: i8,
643 ) -> Result<SetRelativeVolumeResponse, SdkError> {
644 let response = self.exec(
645 rendering_control::set_relative_volume("Master".to_string(), adjustment).build(),
646 )?;
647 self.context
648 .state_manager
649 .set_property(&self.context.speaker_id, Volume(response.new_volume));
650 Ok(response)
651 }
652
653 pub fn set_mute(&self, muted: bool) -> Result<(), SdkError> {
657 self.exec(rendering_control::set_mute("Master".to_string(), muted).build())?;
658 self.context
659 .state_manager
660 .set_property(&self.context.speaker_id, Mute(muted));
661 Ok(())
662 }
663
664 pub fn set_bass(&self, level: i8) -> Result<(), SdkError> {
666 self.exec(rendering_control::set_bass(level).build())?;
667 self.context
668 .state_manager
669 .set_property(&self.context.speaker_id, Bass(level));
670 Ok(())
671 }
672
673 pub fn set_treble(&self, level: i8) -> Result<(), SdkError> {
675 self.exec(rendering_control::set_treble(level).build())?;
676 self.context
677 .state_manager
678 .set_property(&self.context.speaker_id, Treble(level));
679 Ok(())
680 }
681
682 pub fn set_loudness(&self, enabled: bool) -> Result<(), SdkError> {
684 self.exec(rendering_control::set_loudness("Master".to_string(), enabled).build())?;
685 self.context
686 .state_manager
687 .set_property(&self.context.speaker_id, Loudness(enabled));
688 Ok(())
689 }
690}
691
692#[cfg(test)]
693mod tests {
694 use super::*;
695 use sonos_discovery::Device;
696
697 fn create_test_speaker() -> Speaker {
698 let manager = StateManager::new().unwrap();
699 let devices = vec![Device {
700 id: "RINCON_TEST123".to_string(),
701 name: "Test Speaker".to_string(),
702 room_name: "Test Room".to_string(),
703 ip_address: "192.168.1.100".to_string(),
704 port: 1400,
705 model_name: "Sonos One".to_string(),
706 }];
707 manager.add_devices(devices).unwrap();
708 let state_manager = Arc::new(manager);
709 let api_client = SonosClient::new();
710
711 Speaker::new(
712 SpeakerId::new("RINCON_TEST123"),
713 "Test Speaker".to_string(),
714 "192.168.1.100".parse().unwrap(),
715 "Sonos One".to_string(),
716 state_manager,
717 api_client,
718 )
719 }
720
721 #[test]
722 fn test_set_volume_rejects_invalid() {
723 let speaker = create_test_speaker();
724 let result = speaker.set_volume(150);
725 assert!(matches!(result, Err(SdkError::ValidationFailed(_))));
726 }
727
728 #[test]
729 fn test_set_bass_rejects_invalid() {
730 let speaker = create_test_speaker();
731 let result = speaker.set_bass(15);
732 assert!(matches!(result, Err(SdkError::ValidationFailed(_))));
733 }
734
735 #[test]
736 fn test_set_treble_rejects_invalid() {
737 let speaker = create_test_speaker();
738 let result = speaker.set_treble(-15);
739 assert!(matches!(result, Err(SdkError::ValidationFailed(_))));
740 }
741
742 #[test]
743 fn test_speaker_action_methods_exist() {
744 fn assert_void(_r: Result<(), SdkError>) {}
746 fn assert_response<T>(_r: Result<T, SdkError>) {}
747
748 let speaker = create_test_speaker();
749
750 assert_void(speaker.play());
752 assert_void(speaker.pause());
753 assert_void(speaker.stop());
754 assert_void(speaker.next());
755 assert_void(speaker.previous());
756 assert_void(speaker.seek(SeekTarget::Time("0:00:00".into())));
757 assert_void(speaker.set_av_transport_uri("", ""));
758 assert_void(speaker.set_next_av_transport_uri("", ""));
759 assert_response::<GetMediaInfoResponse>(speaker.get_media_info());
760 assert_response::<GetTransportSettingsResponse>(speaker.get_transport_settings());
761 assert_response::<GetCurrentTransportActionsResponse>(
762 speaker.get_current_transport_actions(),
763 );
764 assert_void(speaker.set_play_mode(PlayMode::Normal));
765 assert_response::<GetCrossfadeModeResponse>(speaker.get_crossfade_mode());
766 assert_void(speaker.set_crossfade_mode(true));
767 assert_void(speaker.configure_sleep_timer(""));
768 assert_void(speaker.cancel_sleep_timer());
769 assert_response::<GetRemainingSleepTimerDurationResponse>(
770 speaker.get_remaining_sleep_timer(),
771 );
772 assert_response::<AddURIToQueueResponse>(speaker.add_uri_to_queue("", "", 0, false));
773 assert_void(speaker.remove_track_from_queue("", 0));
774 assert_void(speaker.remove_all_tracks_from_queue());
775 assert_response::<SaveQueueResponse>(speaker.save_queue("", ""));
776 assert_response::<CreateSavedQueueResponse>(speaker.create_saved_queue("", "", ""));
777 assert_response::<RemoveTrackRangeFromQueueResponse>(
778 speaker.remove_track_range_from_queue(0, 0, 1),
779 );
780 assert_void(speaker.backup_queue());
781 assert_response::<GetDeviceCapabilitiesResponse>(speaker.get_device_capabilities());
782 assert_void(speaker.snooze_alarm("00:10:00"));
783 assert_response::<GetRunningAlarmPropertiesResponse>(
784 speaker.get_running_alarm_properties(),
785 );
786 assert_response::<BecomeCoordinatorOfStandaloneGroupResponse>(speaker.become_standalone());
787 assert_void(speaker.delegate_coordination_to(&SpeakerId::new("RINCON_OTHER"), false));
788
789 assert_void(speaker.set_volume(50));
791 assert_response::<SetRelativeVolumeResponse>(speaker.set_relative_volume(5));
792 assert_void(speaker.set_mute(true));
793 assert_void(speaker.set_bass(0));
794 assert_void(speaker.set_treble(0));
795 assert_void(speaker.set_loudness(true));
796
797 let group = create_test_group_for_speaker(&speaker);
799 assert_void(speaker.join_group(&group));
800 assert_response::<BecomeCoordinatorOfStandaloneGroupResponse>(speaker.leave_group());
801 }
802
803 fn create_test_group_for_speaker(speaker: &Speaker) -> crate::Group {
804 use sonos_state::{GroupId, GroupInfo};
805 let state_manager = Arc::new(StateManager::new().unwrap());
806 let devices = vec![Device {
807 id: speaker.id.as_str().to_string(),
808 name: speaker.name.clone(),
809 room_name: speaker.name.clone(),
810 ip_address: speaker.ip.to_string(),
811 port: 1400,
812 model_name: speaker.model_name.clone(),
813 }];
814 state_manager.add_devices(devices).unwrap();
815
816 let group_info = GroupInfo::new(
817 GroupId::new(format!("{}:1", speaker.id.as_str())),
818 speaker.id.clone(),
819 vec![speaker.id.clone()],
820 );
821
822 crate::Group::from_info(group_info, state_manager, SonosClient::new()).unwrap()
823 }
824}