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, GroupMembershipHandle, LoudnessHandle, MuteHandle,
102 PlaybackStateHandle, PositionHandle, PropertyHandle, SpeakerContext, TrebleHandle,
103 VolumeHandle,
104};
105
106#[derive(Clone)]
124pub struct Speaker {
125 pub id: SpeakerId,
127 pub name: String,
129 pub ip: IpAddr,
131 pub model_name: String,
133
134 pub volume: VolumeHandle,
139 pub mute: MuteHandle,
141 pub bass: BassHandle,
143 pub treble: TrebleHandle,
145 pub loudness: LoudnessHandle,
147
148 pub playback_state: PlaybackStateHandle,
153 pub position: PositionHandle,
155 pub current_track: CurrentTrackHandle,
157
158 pub group_membership: GroupMembershipHandle,
163
164 context: Arc<SpeakerContext>,
166}
167
168impl Speaker {
169 pub fn from_device(
185 device: &Device,
186 state_manager: Arc<StateManager>,
187 api_client: SonosClient,
188 ) -> Result<Self, SdkError> {
189 let ip: IpAddr = device
190 .ip_address
191 .parse()
192 .map_err(|_| SdkError::InvalidIpAddress)?;
193
194 let name = if device.room_name.is_empty() || device.room_name == "Unknown" {
195 device.name.clone()
196 } else {
197 device.room_name.clone()
198 };
199
200 Ok(Self::new(
201 SpeakerId::new(&device.id),
202 name,
203 ip,
204 device.model_name.clone(),
205 state_manager,
206 api_client,
207 ))
208 }
209
210 pub fn new(
215 id: SpeakerId,
216 name: String,
217 ip: IpAddr,
218 model_name: String,
219 state_manager: Arc<StateManager>,
220 api_client: SonosClient,
221 ) -> Self {
222 let context = SpeakerContext::new(id.clone(), ip, state_manager, api_client);
223
224 Self {
225 id,
226 name,
227 ip,
228 model_name,
229 volume: PropertyHandle::new(Arc::clone(&context)),
231 mute: PropertyHandle::new(Arc::clone(&context)),
232 bass: PropertyHandle::new(Arc::clone(&context)),
233 treble: PropertyHandle::new(Arc::clone(&context)),
234 loudness: PropertyHandle::new(Arc::clone(&context)),
235 playback_state: PropertyHandle::new(Arc::clone(&context)),
237 position: PropertyHandle::new(Arc::clone(&context)),
238 current_track: PropertyHandle::new(Arc::clone(&context)),
239 group_membership: PropertyHandle::new(Arc::clone(&context)),
241 context,
243 }
244 }
245
246 pub fn group(&self) -> Option<Group> {
264 let info = self
265 .context
266 .state_manager
267 .get_group_for_speaker(&self.context.speaker_id)?;
268 Group::from_info(
269 info,
270 Arc::clone(&self.context.state_manager),
271 self.context.api_client.clone(),
272 )
273 }
274
275 fn exec<Op: UPnPOperation>(
281 &self,
282 operation: Result<ComposableOperation<Op>, ValidationError>,
283 ) -> Result<Op::Response, SdkError> {
284 let op = operation?;
285 self.context
286 .api_client
287 .execute_enhanced(&self.context.speaker_ip.to_string(), op)
288 .map_err(SdkError::ApiError)
289 }
290
291 pub fn play(&self) -> Result<(), SdkError> {
299 self.exec(av_transport::play("1".to_string()).build())?;
300 self.context
301 .state_manager
302 .set_property(&self.context.speaker_id, PlaybackState::Playing);
303 Ok(())
304 }
305
306 pub fn pause(&self) -> Result<(), SdkError> {
310 self.exec(av_transport::pause().build())?;
311 self.context
312 .state_manager
313 .set_property(&self.context.speaker_id, PlaybackState::Paused);
314 Ok(())
315 }
316
317 pub fn stop(&self) -> Result<(), SdkError> {
321 self.exec(av_transport::stop().build())?;
322 self.context
323 .state_manager
324 .set_property(&self.context.speaker_id, PlaybackState::Stopped);
325 Ok(())
326 }
327
328 pub fn next(&self) -> Result<(), SdkError> {
330 self.exec(av_transport::next().build())?;
331 Ok(())
332 }
333
334 pub fn previous(&self) -> Result<(), SdkError> {
336 self.exec(av_transport::previous().build())?;
337 Ok(())
338 }
339
340 pub fn seek(&self, target: SeekTarget) -> Result<(), SdkError> {
354 self.exec(av_transport::seek(target.unit().to_string(), target.target()).build())?;
355 Ok(())
356 }
357
358 pub fn set_av_transport_uri(&self, uri: &str, metadata: &str) -> Result<(), SdkError> {
364 self.exec(
365 av_transport::set_av_transport_uri(uri.to_string(), metadata.to_string()).build(),
366 )?;
367 Ok(())
368 }
369
370 pub fn set_next_av_transport_uri(&self, uri: &str, metadata: &str) -> Result<(), SdkError> {
372 self.exec(
373 av_transport::set_next_av_transport_uri(uri.to_string(), metadata.to_string()).build(),
374 )?;
375 Ok(())
376 }
377
378 pub fn get_media_info(&self) -> Result<GetMediaInfoResponse, SdkError> {
384 self.exec(av_transport::get_media_info().build())
385 }
386
387 pub fn get_transport_settings(&self) -> Result<GetTransportSettingsResponse, SdkError> {
389 self.exec(av_transport::get_transport_settings().build())
390 }
391
392 pub fn get_current_transport_actions(
394 &self,
395 ) -> Result<GetCurrentTransportActionsResponse, SdkError> {
396 self.exec(av_transport::get_current_transport_actions().build())
397 }
398
399 pub fn set_play_mode(&self, mode: PlayMode) -> Result<(), SdkError> {
412 self.exec(av_transport::set_play_mode(mode.to_string()).build())?;
413 Ok(())
414 }
415
416 pub fn get_crossfade_mode(&self) -> Result<GetCrossfadeModeResponse, SdkError> {
418 self.exec(av_transport::get_crossfade_mode().build())
419 }
420
421 pub fn set_crossfade_mode(&self, enabled: bool) -> Result<(), SdkError> {
423 self.exec(av_transport::set_crossfade_mode(enabled).build())?;
424 Ok(())
425 }
426
427 pub fn configure_sleep_timer(&self, duration: &str) -> Result<(), SdkError> {
433 self.exec(av_transport::configure_sleep_timer(duration.to_string()).build())?;
434 Ok(())
435 }
436
437 pub fn cancel_sleep_timer(&self) -> Result<(), SdkError> {
439 self.configure_sleep_timer("")
440 }
441
442 pub fn get_remaining_sleep_timer(
444 &self,
445 ) -> Result<GetRemainingSleepTimerDurationResponse, SdkError> {
446 self.exec(av_transport::get_remaining_sleep_timer_duration().build())
447 }
448
449 pub fn add_uri_to_queue(
455 &self,
456 uri: &str,
457 metadata: &str,
458 position: u32,
459 enqueue_as_next: bool,
460 ) -> Result<AddURIToQueueResponse, SdkError> {
461 self.exec(
462 av_transport::add_uri_to_queue(
463 uri.to_string(),
464 metadata.to_string(),
465 position,
466 enqueue_as_next,
467 )
468 .build(),
469 )
470 }
471
472 pub fn remove_track_from_queue(&self, object_id: &str, update_id: u32) -> Result<(), SdkError> {
474 self.exec(av_transport::remove_track_from_queue(object_id.to_string(), update_id).build())?;
475 Ok(())
476 }
477
478 pub fn remove_all_tracks_from_queue(&self) -> Result<(), SdkError> {
480 self.exec(av_transport::remove_all_tracks_from_queue().build())?;
481 Ok(())
482 }
483
484 pub fn save_queue(&self, title: &str, object_id: &str) -> Result<SaveQueueResponse, SdkError> {
486 self.exec(av_transport::save_queue(title.to_string(), object_id.to_string()).build())
487 }
488
489 pub fn create_saved_queue(
491 &self,
492 title: &str,
493 uri: &str,
494 metadata: &str,
495 ) -> Result<CreateSavedQueueResponse, SdkError> {
496 self.exec(
497 av_transport::create_saved_queue(
498 title.to_string(),
499 uri.to_string(),
500 metadata.to_string(),
501 )
502 .build(),
503 )
504 }
505
506 pub fn remove_track_range_from_queue(
508 &self,
509 update_id: u32,
510 starting_index: u32,
511 number_of_tracks: u32,
512 ) -> Result<RemoveTrackRangeFromQueueResponse, SdkError> {
513 self.exec(
514 av_transport::remove_track_range_from_queue(
515 update_id,
516 starting_index,
517 number_of_tracks,
518 )
519 .build(),
520 )
521 }
522
523 pub fn backup_queue(&self) -> Result<(), SdkError> {
525 self.exec(av_transport::backup_queue().build())?;
526 Ok(())
527 }
528
529 pub fn get_device_capabilities(&self) -> Result<GetDeviceCapabilitiesResponse, SdkError> {
535 self.exec(av_transport::get_device_capabilities().build())
536 }
537
538 pub fn snooze_alarm(&self, duration: &str) -> Result<(), SdkError> {
544 self.exec(av_transport::snooze_alarm(duration.to_string()).build())?;
545 Ok(())
546 }
547
548 pub fn get_running_alarm_properties(
550 &self,
551 ) -> Result<GetRunningAlarmPropertiesResponse, SdkError> {
552 self.exec(av_transport::get_running_alarm_properties().build())
553 }
554
555 pub fn become_standalone(
561 &self,
562 ) -> Result<BecomeCoordinatorOfStandaloneGroupResponse, SdkError> {
563 self.exec(av_transport::become_coordinator_of_standalone_group().build())
564 }
565
566 pub fn delegate_coordination_to(
568 &self,
569 new_coordinator: &SpeakerId,
570 rejoin_group: bool,
571 ) -> Result<(), SdkError> {
572 self.exec(
573 av_transport::delegate_group_coordination_to(
574 new_coordinator.as_str().to_string(),
575 rejoin_group,
576 )
577 .build(),
578 )?;
579 Ok(())
580 }
581
582 pub fn join_group(&self, group: &Group) -> Result<(), SdkError> {
587 group.add_speaker(self)
588 }
589
590 pub fn leave_group(&self) -> Result<BecomeCoordinatorOfStandaloneGroupResponse, SdkError> {
595 self.become_standalone()
596 }
597
598 pub fn set_volume(&self, volume: u8) -> Result<(), SdkError> {
606 self.exec(rendering_control::set_volume("Master".to_string(), volume).build())?;
607 self.context
608 .state_manager
609 .set_property(&self.context.speaker_id, Volume(volume));
610 Ok(())
611 }
612
613 pub fn set_relative_volume(
617 &self,
618 adjustment: i8,
619 ) -> Result<SetRelativeVolumeResponse, SdkError> {
620 let response = self.exec(
621 rendering_control::set_relative_volume("Master".to_string(), adjustment).build(),
622 )?;
623 self.context
624 .state_manager
625 .set_property(&self.context.speaker_id, Volume(response.new_volume));
626 Ok(response)
627 }
628
629 pub fn set_mute(&self, muted: bool) -> Result<(), SdkError> {
633 self.exec(rendering_control::set_mute("Master".to_string(), muted).build())?;
634 self.context
635 .state_manager
636 .set_property(&self.context.speaker_id, Mute(muted));
637 Ok(())
638 }
639
640 pub fn set_bass(&self, level: i8) -> Result<(), SdkError> {
642 self.exec(rendering_control::set_bass(level).build())?;
643 self.context
644 .state_manager
645 .set_property(&self.context.speaker_id, Bass(level));
646 Ok(())
647 }
648
649 pub fn set_treble(&self, level: i8) -> Result<(), SdkError> {
651 self.exec(rendering_control::set_treble(level).build())?;
652 self.context
653 .state_manager
654 .set_property(&self.context.speaker_id, Treble(level));
655 Ok(())
656 }
657
658 pub fn set_loudness(&self, enabled: bool) -> Result<(), SdkError> {
660 self.exec(rendering_control::set_loudness("Master".to_string(), enabled).build())?;
661 self.context
662 .state_manager
663 .set_property(&self.context.speaker_id, Loudness(enabled));
664 Ok(())
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 use sonos_discovery::Device;
672
673 fn create_test_speaker() -> Speaker {
674 let manager = StateManager::new().unwrap();
675 let devices = vec![Device {
676 id: "RINCON_TEST123".to_string(),
677 name: "Test Speaker".to_string(),
678 room_name: "Test Room".to_string(),
679 ip_address: "192.168.1.100".to_string(),
680 port: 1400,
681 model_name: "Sonos One".to_string(),
682 }];
683 manager.add_devices(devices).unwrap();
684 let state_manager = Arc::new(manager);
685 let api_client = SonosClient::new();
686
687 Speaker::new(
688 SpeakerId::new("RINCON_TEST123"),
689 "Test Speaker".to_string(),
690 "192.168.1.100".parse().unwrap(),
691 "Sonos One".to_string(),
692 state_manager,
693 api_client,
694 )
695 }
696
697 #[test]
698 fn test_set_volume_rejects_invalid() {
699 let speaker = create_test_speaker();
700 let result = speaker.set_volume(150);
701 assert!(matches!(result, Err(SdkError::ValidationFailed(_))));
702 }
703
704 #[test]
705 fn test_set_bass_rejects_invalid() {
706 let speaker = create_test_speaker();
707 let result = speaker.set_bass(15);
708 assert!(matches!(result, Err(SdkError::ValidationFailed(_))));
709 }
710
711 #[test]
712 fn test_set_treble_rejects_invalid() {
713 let speaker = create_test_speaker();
714 let result = speaker.set_treble(-15);
715 assert!(matches!(result, Err(SdkError::ValidationFailed(_))));
716 }
717
718 #[test]
719 fn test_speaker_action_methods_exist() {
720 fn assert_void(_r: Result<(), SdkError>) {}
722 fn assert_response<T>(_r: Result<T, SdkError>) {}
723
724 let speaker = create_test_speaker();
725
726 assert_void(speaker.play());
728 assert_void(speaker.pause());
729 assert_void(speaker.stop());
730 assert_void(speaker.next());
731 assert_void(speaker.previous());
732 assert_void(speaker.seek(SeekTarget::Time("0:00:00".into())));
733 assert_void(speaker.set_av_transport_uri("", ""));
734 assert_void(speaker.set_next_av_transport_uri("", ""));
735 assert_response::<GetMediaInfoResponse>(speaker.get_media_info());
736 assert_response::<GetTransportSettingsResponse>(speaker.get_transport_settings());
737 assert_response::<GetCurrentTransportActionsResponse>(
738 speaker.get_current_transport_actions(),
739 );
740 assert_void(speaker.set_play_mode(PlayMode::Normal));
741 assert_response::<GetCrossfadeModeResponse>(speaker.get_crossfade_mode());
742 assert_void(speaker.set_crossfade_mode(true));
743 assert_void(speaker.configure_sleep_timer(""));
744 assert_void(speaker.cancel_sleep_timer());
745 assert_response::<GetRemainingSleepTimerDurationResponse>(
746 speaker.get_remaining_sleep_timer(),
747 );
748 assert_response::<AddURIToQueueResponse>(speaker.add_uri_to_queue("", "", 0, false));
749 assert_void(speaker.remove_track_from_queue("", 0));
750 assert_void(speaker.remove_all_tracks_from_queue());
751 assert_response::<SaveQueueResponse>(speaker.save_queue("", ""));
752 assert_response::<CreateSavedQueueResponse>(speaker.create_saved_queue("", "", ""));
753 assert_response::<RemoveTrackRangeFromQueueResponse>(
754 speaker.remove_track_range_from_queue(0, 0, 1),
755 );
756 assert_void(speaker.backup_queue());
757 assert_response::<GetDeviceCapabilitiesResponse>(speaker.get_device_capabilities());
758 assert_void(speaker.snooze_alarm("00:10:00"));
759 assert_response::<GetRunningAlarmPropertiesResponse>(
760 speaker.get_running_alarm_properties(),
761 );
762 assert_response::<BecomeCoordinatorOfStandaloneGroupResponse>(speaker.become_standalone());
763 assert_void(speaker.delegate_coordination_to(&SpeakerId::new("RINCON_OTHER"), false));
764
765 assert_void(speaker.set_volume(50));
767 assert_response::<SetRelativeVolumeResponse>(speaker.set_relative_volume(5));
768 assert_void(speaker.set_mute(true));
769 assert_void(speaker.set_bass(0));
770 assert_void(speaker.set_treble(0));
771 assert_void(speaker.set_loudness(true));
772
773 let group = create_test_group_for_speaker(&speaker);
775 assert_void(speaker.join_group(&group));
776 assert_response::<BecomeCoordinatorOfStandaloneGroupResponse>(speaker.leave_group());
777 }
778
779 fn create_test_group_for_speaker(speaker: &Speaker) -> crate::Group {
780 use sonos_state::{GroupId, GroupInfo};
781 let state_manager = Arc::new(StateManager::new().unwrap());
782 let devices = vec![Device {
783 id: speaker.id.as_str().to_string(),
784 name: speaker.name.clone(),
785 room_name: speaker.name.clone(),
786 ip_address: speaker.ip.to_string(),
787 port: 1400,
788 model_name: speaker.model_name.clone(),
789 }];
790 state_manager.add_devices(devices).unwrap();
791
792 let group_info = GroupInfo::new(
793 GroupId::new(format!("{}:1", speaker.id.as_str())),
794 speaker.id.clone(),
795 vec![speaker.id.clone()],
796 );
797
798 crate::Group::from_info(group_info, state_manager, SonosClient::new()).unwrap()
799 }
800}