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)]
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 Self::new_with_event_init(id, name, ip, model_name, state_manager, api_client, None)
223 }
224
225 pub(crate) fn new_with_event_init(
230 id: SpeakerId,
231 name: String,
232 ip: IpAddr,
233 model_name: String,
234 state_manager: Arc<StateManager>,
235 api_client: SonosClient,
236 event_init: Option<EventInitFn>,
237 ) -> Self {
238 let context = match event_init {
239 Some(init) => {
240 SpeakerContext::with_event_init(id.clone(), ip, state_manager, api_client, init)
241 }
242 None => SpeakerContext::new(id.clone(), ip, state_manager, api_client),
243 };
244
245 Self {
246 id,
247 name,
248 ip,
249 model_name,
250 volume: PropertyHandle::new(Arc::clone(&context)),
252 mute: PropertyHandle::new(Arc::clone(&context)),
253 bass: PropertyHandle::new(Arc::clone(&context)),
254 treble: PropertyHandle::new(Arc::clone(&context)),
255 loudness: PropertyHandle::new(Arc::clone(&context)),
256 playback_state: PropertyHandle::new(Arc::clone(&context)),
258 position: PropertyHandle::new(Arc::clone(&context)),
259 current_track: PropertyHandle::new(Arc::clone(&context)),
260 group_membership: PropertyHandle::new(Arc::clone(&context)),
262 context,
264 }
265 }
266
267 pub fn group(&self) -> Option<Group> {
285 let info = self
286 .context
287 .state_manager
288 .get_group_for_speaker(&self.context.speaker_id)?;
289 Group::from_info(
290 info,
291 Arc::clone(&self.context.state_manager),
292 self.context.api_client.clone(),
293 )
294 }
295
296 fn exec<Op: UPnPOperation>(
302 &self,
303 operation: Result<ComposableOperation<Op>, ValidationError>,
304 ) -> Result<Op::Response, SdkError> {
305 let op = operation?;
306 self.context
307 .api_client
308 .execute_enhanced(&self.context.speaker_ip.to_string(), op)
309 .map_err(SdkError::ApiError)
310 }
311
312 pub fn play(&self) -> Result<(), SdkError> {
320 self.exec(av_transport::play("1".to_string()).build())?;
321 self.context
322 .state_manager
323 .set_property(&self.context.speaker_id, PlaybackState::Playing);
324 Ok(())
325 }
326
327 pub fn pause(&self) -> Result<(), SdkError> {
331 self.exec(av_transport::pause().build())?;
332 self.context
333 .state_manager
334 .set_property(&self.context.speaker_id, PlaybackState::Paused);
335 Ok(())
336 }
337
338 pub fn stop(&self) -> Result<(), SdkError> {
342 self.exec(av_transport::stop().build())?;
343 self.context
344 .state_manager
345 .set_property(&self.context.speaker_id, PlaybackState::Stopped);
346 Ok(())
347 }
348
349 pub fn next(&self) -> Result<(), SdkError> {
351 self.exec(av_transport::next().build())?;
352 Ok(())
353 }
354
355 pub fn previous(&self) -> Result<(), SdkError> {
357 self.exec(av_transport::previous().build())?;
358 Ok(())
359 }
360
361 pub fn seek(&self, target: SeekTarget) -> Result<(), SdkError> {
375 self.exec(av_transport::seek(target.unit().to_string(), target.target()).build())?;
376 Ok(())
377 }
378
379 pub fn set_av_transport_uri(&self, uri: &str, metadata: &str) -> Result<(), SdkError> {
385 self.exec(
386 av_transport::set_av_transport_uri(uri.to_string(), metadata.to_string()).build(),
387 )?;
388 Ok(())
389 }
390
391 pub fn set_next_av_transport_uri(&self, uri: &str, metadata: &str) -> Result<(), SdkError> {
393 self.exec(
394 av_transport::set_next_av_transport_uri(uri.to_string(), metadata.to_string()).build(),
395 )?;
396 Ok(())
397 }
398
399 pub fn get_media_info(&self) -> Result<GetMediaInfoResponse, SdkError> {
405 self.exec(av_transport::get_media_info().build())
406 }
407
408 pub fn get_transport_settings(&self) -> Result<GetTransportSettingsResponse, SdkError> {
410 self.exec(av_transport::get_transport_settings().build())
411 }
412
413 pub fn get_current_transport_actions(
415 &self,
416 ) -> Result<GetCurrentTransportActionsResponse, SdkError> {
417 self.exec(av_transport::get_current_transport_actions().build())
418 }
419
420 pub fn set_play_mode(&self, mode: PlayMode) -> Result<(), SdkError> {
433 self.exec(av_transport::set_play_mode(mode.to_string()).build())?;
434 Ok(())
435 }
436
437 pub fn get_crossfade_mode(&self) -> Result<GetCrossfadeModeResponse, SdkError> {
439 self.exec(av_transport::get_crossfade_mode().build())
440 }
441
442 pub fn set_crossfade_mode(&self, enabled: bool) -> Result<(), SdkError> {
444 self.exec(av_transport::set_crossfade_mode(enabled).build())?;
445 Ok(())
446 }
447
448 pub fn configure_sleep_timer(&self, duration: &str) -> Result<(), SdkError> {
454 self.exec(av_transport::configure_sleep_timer(duration.to_string()).build())?;
455 Ok(())
456 }
457
458 pub fn cancel_sleep_timer(&self) -> Result<(), SdkError> {
460 self.configure_sleep_timer("")
461 }
462
463 pub fn get_remaining_sleep_timer(
465 &self,
466 ) -> Result<GetRemainingSleepTimerDurationResponse, SdkError> {
467 self.exec(av_transport::get_remaining_sleep_timer_duration().build())
468 }
469
470 pub fn add_uri_to_queue(
476 &self,
477 uri: &str,
478 metadata: &str,
479 position: u32,
480 enqueue_as_next: bool,
481 ) -> Result<AddURIToQueueResponse, SdkError> {
482 self.exec(
483 av_transport::add_uri_to_queue(
484 uri.to_string(),
485 metadata.to_string(),
486 position,
487 enqueue_as_next,
488 )
489 .build(),
490 )
491 }
492
493 pub fn remove_track_from_queue(&self, object_id: &str, update_id: u32) -> Result<(), SdkError> {
495 self.exec(av_transport::remove_track_from_queue(object_id.to_string(), update_id).build())?;
496 Ok(())
497 }
498
499 pub fn remove_all_tracks_from_queue(&self) -> Result<(), SdkError> {
501 self.exec(av_transport::remove_all_tracks_from_queue().build())?;
502 Ok(())
503 }
504
505 pub fn save_queue(&self, title: &str, object_id: &str) -> Result<SaveQueueResponse, SdkError> {
507 self.exec(av_transport::save_queue(title.to_string(), object_id.to_string()).build())
508 }
509
510 pub fn create_saved_queue(
512 &self,
513 title: &str,
514 uri: &str,
515 metadata: &str,
516 ) -> Result<CreateSavedQueueResponse, SdkError> {
517 self.exec(
518 av_transport::create_saved_queue(
519 title.to_string(),
520 uri.to_string(),
521 metadata.to_string(),
522 )
523 .build(),
524 )
525 }
526
527 pub fn remove_track_range_from_queue(
529 &self,
530 update_id: u32,
531 starting_index: u32,
532 number_of_tracks: u32,
533 ) -> Result<RemoveTrackRangeFromQueueResponse, SdkError> {
534 self.exec(
535 av_transport::remove_track_range_from_queue(
536 update_id,
537 starting_index,
538 number_of_tracks,
539 )
540 .build(),
541 )
542 }
543
544 pub fn backup_queue(&self) -> Result<(), SdkError> {
546 self.exec(av_transport::backup_queue().build())?;
547 Ok(())
548 }
549
550 pub fn get_device_capabilities(&self) -> Result<GetDeviceCapabilitiesResponse, SdkError> {
556 self.exec(av_transport::get_device_capabilities().build())
557 }
558
559 pub fn snooze_alarm(&self, duration: &str) -> Result<(), SdkError> {
565 self.exec(av_transport::snooze_alarm(duration.to_string()).build())?;
566 Ok(())
567 }
568
569 pub fn get_running_alarm_properties(
571 &self,
572 ) -> Result<GetRunningAlarmPropertiesResponse, SdkError> {
573 self.exec(av_transport::get_running_alarm_properties().build())
574 }
575
576 pub fn become_standalone(
582 &self,
583 ) -> Result<BecomeCoordinatorOfStandaloneGroupResponse, SdkError> {
584 self.exec(av_transport::become_coordinator_of_standalone_group().build())
585 }
586
587 pub fn delegate_coordination_to(
589 &self,
590 new_coordinator: &SpeakerId,
591 rejoin_group: bool,
592 ) -> Result<(), SdkError> {
593 self.exec(
594 av_transport::delegate_group_coordination_to(
595 new_coordinator.as_str().to_string(),
596 rejoin_group,
597 )
598 .build(),
599 )?;
600 Ok(())
601 }
602
603 pub fn join_group(&self, group: &Group) -> Result<(), SdkError> {
608 group.add_speaker(self)
609 }
610
611 pub fn leave_group(&self) -> Result<BecomeCoordinatorOfStandaloneGroupResponse, SdkError> {
616 self.become_standalone()
617 }
618
619 pub fn set_volume(&self, volume: u8) -> Result<(), SdkError> {
627 self.exec(rendering_control::set_volume("Master".to_string(), volume).build())?;
628 self.context
629 .state_manager
630 .set_property(&self.context.speaker_id, Volume(volume));
631 Ok(())
632 }
633
634 pub fn set_relative_volume(
638 &self,
639 adjustment: i8,
640 ) -> Result<SetRelativeVolumeResponse, SdkError> {
641 let response = self.exec(
642 rendering_control::set_relative_volume("Master".to_string(), adjustment).build(),
643 )?;
644 self.context
645 .state_manager
646 .set_property(&self.context.speaker_id, Volume(response.new_volume));
647 Ok(response)
648 }
649
650 pub fn set_mute(&self, muted: bool) -> Result<(), SdkError> {
654 self.exec(rendering_control::set_mute("Master".to_string(), muted).build())?;
655 self.context
656 .state_manager
657 .set_property(&self.context.speaker_id, Mute(muted));
658 Ok(())
659 }
660
661 pub fn set_bass(&self, level: i8) -> Result<(), SdkError> {
663 self.exec(rendering_control::set_bass(level).build())?;
664 self.context
665 .state_manager
666 .set_property(&self.context.speaker_id, Bass(level));
667 Ok(())
668 }
669
670 pub fn set_treble(&self, level: i8) -> Result<(), SdkError> {
672 self.exec(rendering_control::set_treble(level).build())?;
673 self.context
674 .state_manager
675 .set_property(&self.context.speaker_id, Treble(level));
676 Ok(())
677 }
678
679 pub fn set_loudness(&self, enabled: bool) -> Result<(), SdkError> {
681 self.exec(rendering_control::set_loudness("Master".to_string(), enabled).build())?;
682 self.context
683 .state_manager
684 .set_property(&self.context.speaker_id, Loudness(enabled));
685 Ok(())
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692 use sonos_discovery::Device;
693
694 fn create_test_speaker() -> Speaker {
695 let manager = StateManager::new().unwrap();
696 let devices = vec![Device {
697 id: "RINCON_TEST123".to_string(),
698 name: "Test Speaker".to_string(),
699 room_name: "Test Room".to_string(),
700 ip_address: "192.168.1.100".to_string(),
701 port: 1400,
702 model_name: "Sonos One".to_string(),
703 }];
704 manager.add_devices(devices).unwrap();
705 let state_manager = Arc::new(manager);
706 let api_client = SonosClient::new();
707
708 Speaker::new(
709 SpeakerId::new("RINCON_TEST123"),
710 "Test Speaker".to_string(),
711 "192.168.1.100".parse().unwrap(),
712 "Sonos One".to_string(),
713 state_manager,
714 api_client,
715 )
716 }
717
718 #[test]
719 fn test_set_volume_rejects_invalid() {
720 let speaker = create_test_speaker();
721 let result = speaker.set_volume(150);
722 assert!(matches!(result, Err(SdkError::ValidationFailed(_))));
723 }
724
725 #[test]
726 fn test_set_bass_rejects_invalid() {
727 let speaker = create_test_speaker();
728 let result = speaker.set_bass(15);
729 assert!(matches!(result, Err(SdkError::ValidationFailed(_))));
730 }
731
732 #[test]
733 fn test_set_treble_rejects_invalid() {
734 let speaker = create_test_speaker();
735 let result = speaker.set_treble(-15);
736 assert!(matches!(result, Err(SdkError::ValidationFailed(_))));
737 }
738
739 #[test]
740 fn test_speaker_action_methods_exist() {
741 fn assert_void(_r: Result<(), SdkError>) {}
743 fn assert_response<T>(_r: Result<T, SdkError>) {}
744
745 let speaker = create_test_speaker();
746
747 assert_void(speaker.play());
749 assert_void(speaker.pause());
750 assert_void(speaker.stop());
751 assert_void(speaker.next());
752 assert_void(speaker.previous());
753 assert_void(speaker.seek(SeekTarget::Time("0:00:00".into())));
754 assert_void(speaker.set_av_transport_uri("", ""));
755 assert_void(speaker.set_next_av_transport_uri("", ""));
756 assert_response::<GetMediaInfoResponse>(speaker.get_media_info());
757 assert_response::<GetTransportSettingsResponse>(speaker.get_transport_settings());
758 assert_response::<GetCurrentTransportActionsResponse>(
759 speaker.get_current_transport_actions(),
760 );
761 assert_void(speaker.set_play_mode(PlayMode::Normal));
762 assert_response::<GetCrossfadeModeResponse>(speaker.get_crossfade_mode());
763 assert_void(speaker.set_crossfade_mode(true));
764 assert_void(speaker.configure_sleep_timer(""));
765 assert_void(speaker.cancel_sleep_timer());
766 assert_response::<GetRemainingSleepTimerDurationResponse>(
767 speaker.get_remaining_sleep_timer(),
768 );
769 assert_response::<AddURIToQueueResponse>(speaker.add_uri_to_queue("", "", 0, false));
770 assert_void(speaker.remove_track_from_queue("", 0));
771 assert_void(speaker.remove_all_tracks_from_queue());
772 assert_response::<SaveQueueResponse>(speaker.save_queue("", ""));
773 assert_response::<CreateSavedQueueResponse>(speaker.create_saved_queue("", "", ""));
774 assert_response::<RemoveTrackRangeFromQueueResponse>(
775 speaker.remove_track_range_from_queue(0, 0, 1),
776 );
777 assert_void(speaker.backup_queue());
778 assert_response::<GetDeviceCapabilitiesResponse>(speaker.get_device_capabilities());
779 assert_void(speaker.snooze_alarm("00:10:00"));
780 assert_response::<GetRunningAlarmPropertiesResponse>(
781 speaker.get_running_alarm_properties(),
782 );
783 assert_response::<BecomeCoordinatorOfStandaloneGroupResponse>(speaker.become_standalone());
784 assert_void(speaker.delegate_coordination_to(&SpeakerId::new("RINCON_OTHER"), false));
785
786 assert_void(speaker.set_volume(50));
788 assert_response::<SetRelativeVolumeResponse>(speaker.set_relative_volume(5));
789 assert_void(speaker.set_mute(true));
790 assert_void(speaker.set_bass(0));
791 assert_void(speaker.set_treble(0));
792 assert_void(speaker.set_loudness(true));
793
794 let group = create_test_group_for_speaker(&speaker);
796 assert_void(speaker.join_group(&group));
797 assert_response::<BecomeCoordinatorOfStandaloneGroupResponse>(speaker.leave_group());
798 }
799
800 fn create_test_group_for_speaker(speaker: &Speaker) -> crate::Group {
801 use sonos_state::{GroupId, GroupInfo};
802 let state_manager = Arc::new(StateManager::new().unwrap());
803 let devices = vec![Device {
804 id: speaker.id.as_str().to_string(),
805 name: speaker.name.clone(),
806 room_name: speaker.name.clone(),
807 ip_address: speaker.ip.to_string(),
808 port: 1400,
809 model_name: speaker.model_name.clone(),
810 }];
811 state_manager.add_devices(devices).unwrap();
812
813 let group_info = GroupInfo::new(
814 GroupId::new(format!("{}:1", speaker.id.as_str())),
815 speaker.id.clone(),
816 vec![speaker.id.clone()],
817 );
818
819 crate::Group::from_info(group_info, state_manager, SonosClient::new()).unwrap()
820 }
821}