1use crate::{define_operation_with_response, define_upnp_operation, Validate};
7use paste::paste;
8
9define_upnp_operation! {
14 operation: PlayOperation,
15 action: "Play",
16 service: AVTransport,
17 request: {
18 speed: String,
19 },
20 response: (),
21 payload: |req| {
22 format!("<InstanceID>{}</InstanceID><Speed>{}</Speed>", req.instance_id, req.speed)
23 },
24 parse: |_xml| Ok(()),
25}
26
27impl Validate for PlayOperationRequest {
28 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
29 if self.speed.is_empty() {
30 return Err(crate::operation::ValidationError::invalid_value(
31 "speed",
32 &self.speed,
33 ));
34 }
35 match self.speed.as_str() {
36 "1" | "0" => Ok(()),
37 other => {
38 if other.parse::<f32>().is_ok() {
39 Ok(())
40 } else {
41 Err(crate::operation::ValidationError::Custom {
42 parameter: "speed".to_string(),
43 message: "Speed must be '1', '0', or a numeric value".to_string(),
44 })
45 }
46 }
47 }
48 }
49}
50
51define_upnp_operation! {
52 operation: PauseOperation,
53 action: "Pause",
54 service: AVTransport,
55 request: {},
56 response: (),
57 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
58 parse: |_xml| Ok(()),
59}
60
61impl Validate for PauseOperationRequest {}
62
63define_upnp_operation! {
64 operation: StopOperation,
65 action: "Stop",
66 service: AVTransport,
67 request: {},
68 response: (),
69 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
70 parse: |_xml| Ok(()),
71}
72
73impl Validate for StopOperationRequest {}
74
75define_upnp_operation! {
76 operation: NextOperation,
77 action: "Next",
78 service: AVTransport,
79 request: {},
80 response: (),
81 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
82 parse: |_xml| Ok(()),
83}
84
85impl Validate for NextOperationRequest {}
86
87define_upnp_operation! {
88 operation: PreviousOperation,
89 action: "Previous",
90 service: AVTransport,
91 request: {},
92 response: (),
93 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
94 parse: |_xml| Ok(()),
95}
96
97impl Validate for PreviousOperationRequest {}
98
99define_upnp_operation! {
104 operation: SeekOperation,
105 action: "Seek",
106 service: AVTransport,
107 request: {
108 unit: String,
109 target: String,
110 },
111 response: (),
112 payload: |req| {
113 format!(
114 "<InstanceID>{}</InstanceID><Unit>{}</Unit><Target>{}</Target>",
115 req.instance_id,
116 crate::operation::xml_escape(&req.unit),
117 crate::operation::xml_escape(&req.target)
118 )
119 },
120 parse: |_xml| Ok(()),
121}
122
123impl Validate for SeekOperationRequest {
124 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
125 match self.unit.as_str() {
126 "TRACK_NR" | "REL_TIME" | "TIME_DELTA" => Ok(()),
127 other => Err(crate::operation::ValidationError::Custom {
128 parameter: "unit".to_string(),
129 message: format!(
130 "Invalid unit '{other}'. Must be 'TRACK_NR', 'REL_TIME', or 'TIME_DELTA'"
131 ),
132 }),
133 }
134 }
135}
136
137define_operation_with_response! {
138 operation: GetPositionInfoOperation,
139 action: "GetPositionInfo",
140 service: AVTransport,
141 request: {},
142 response: GetPositionInfoResponse {
143 track: u32,
144 track_duration: String,
145 track_meta_data: String,
146 track_uri: String,
147 rel_time: String,
148 abs_time: String,
149 rel_count: i32,
150 abs_count: i32,
151 },
152 xml_mapping: {
153 track: "Track",
154 track_duration: "TrackDuration",
155 track_meta_data: "TrackMetaData",
156 track_uri: "TrackURI",
157 rel_time: "RelTime",
158 abs_time: "AbsTime",
159 rel_count: "RelCount",
160 abs_count: "AbsCount",
161 },
162}
163
164impl Validate for GetPositionInfoOperationRequest {}
165
166define_operation_with_response! {
171 operation: GetTransportInfoOperation,
172 action: "GetTransportInfo",
173 service: AVTransport,
174 request: {},
175 response: GetTransportInfoResponse {
176 current_transport_state: String,
177 current_transport_status: String,
178 current_speed: String,
179 },
180 xml_mapping: {
181 current_transport_state: "CurrentTransportState",
182 current_transport_status: "CurrentTransportStatus",
183 current_speed: "CurrentSpeed",
184 },
185}
186
187impl Validate for GetTransportInfoOperationRequest {}
188
189define_operation_with_response! {
190 operation: GetTransportSettingsOperation,
191 action: "GetTransportSettings",
192 service: AVTransport,
193 request: {},
194 response: GetTransportSettingsResponse {
195 play_mode: String,
196 rec_quality_mode: String,
197 },
198 xml_mapping: {
199 play_mode: "PlayMode",
200 rec_quality_mode: "RecQualityMode",
201 },
202}
203
204impl Validate for GetTransportSettingsOperationRequest {}
205
206define_operation_with_response! {
207 operation: GetCurrentTransportActionsOperation,
208 action: "GetCurrentTransportActions",
209 service: AVTransport,
210 request: {},
211 response: GetCurrentTransportActionsResponse {
212 actions: String,
213 },
214 xml_mapping: {
215 actions: "Actions",
216 },
217}
218
219impl Validate for GetCurrentTransportActionsOperationRequest {}
220
221define_operation_with_response! {
222 operation: GetDeviceCapabilitiesOperation,
223 action: "GetDeviceCapabilities",
224 service: AVTransport,
225 request: {},
226 response: GetDeviceCapabilitiesResponse {
227 play_media: String,
228 rec_media: String,
229 rec_quality_modes: String,
230 },
231 xml_mapping: {
232 play_media: "PlayMedia",
233 rec_media: "RecMedia",
234 rec_quality_modes: "RecQualityModes",
235 },
236}
237
238impl Validate for GetDeviceCapabilitiesOperationRequest {}
239
240define_operation_with_response! {
245 operation: GetMediaInfoOperation,
246 action: "GetMediaInfo",
247 service: AVTransport,
248 request: {},
249 response: GetMediaInfoResponse {
250 nr_tracks: u32,
251 media_duration: String,
252 current_uri: String,
253 current_uri_meta_data: String,
254 next_uri: String,
255 next_uri_meta_data: String,
256 play_medium: String,
257 record_medium: String,
258 write_status: String,
259 },
260 xml_mapping: {
261 nr_tracks: "NrTracks",
262 media_duration: "MediaDuration",
263 current_uri: "CurrentURI",
264 current_uri_meta_data: "CurrentURIMetaData",
265 next_uri: "NextURI",
266 next_uri_meta_data: "NextURIMetaData",
267 play_medium: "PlayMedium",
268 record_medium: "RecordMedium",
269 write_status: "WriteStatus",
270 },
271}
272
273impl Validate for GetMediaInfoOperationRequest {}
274
275define_upnp_operation! {
276 operation: SetAVTransportURIOperation,
277 action: "SetAVTransportURI",
278 service: AVTransport,
279 request: {
280 current_uri: String,
281 current_uri_meta_data: String,
282 },
283 response: (),
284 payload: |req| {
285 format!(
286 "<InstanceID>{}</InstanceID><CurrentURI>{}</CurrentURI><CurrentURIMetaData>{}</CurrentURIMetaData>",
287 req.instance_id,
288 crate::operation::xml_escape(&req.current_uri),
289 crate::operation::xml_escape(&req.current_uri_meta_data)
290 )
291 },
292 parse: |_xml| Ok(()),
293}
294
295impl Validate for SetAVTransportURIOperationRequest {}
296
297define_upnp_operation! {
298 operation: SetNextAVTransportURIOperation,
299 action: "SetNextAVTransportURI",
300 service: AVTransport,
301 request: {
302 next_uri: String,
303 next_uri_meta_data: String,
304 },
305 response: (),
306 payload: |req| {
307 format!(
308 "<InstanceID>{}</InstanceID><NextURI>{}</NextURI><NextURIMetaData>{}</NextURIMetaData>",
309 req.instance_id,
310 crate::operation::xml_escape(&req.next_uri),
311 crate::operation::xml_escape(&req.next_uri_meta_data)
312 )
313 },
314 parse: |_xml| Ok(()),
315}
316
317impl Validate for SetNextAVTransportURIOperationRequest {}
318
319define_operation_with_response! {
324 operation: GetCrossfadeModeOperation,
325 action: "GetCrossfadeMode",
326 service: AVTransport,
327 request: {},
328 response: GetCrossfadeModeResponse {
329 crossfade_mode: String,
330 },
331 xml_mapping: {
332 crossfade_mode: "CrossfadeMode",
333 },
334}
335
336impl Validate for GetCrossfadeModeOperationRequest {}
337
338define_upnp_operation! {
339 operation: SetCrossfadeModeOperation,
340 action: "SetCrossfadeMode",
341 service: AVTransport,
342 request: {
343 crossfade_mode: bool,
344 },
345 response: (),
346 payload: |req| {
347 format!(
348 "<InstanceID>{}</InstanceID><CrossfadeMode>{}</CrossfadeMode>",
349 req.instance_id,
350 if req.crossfade_mode { "1" } else { "0" }
351 )
352 },
353 parse: |_xml| Ok(()),
354}
355
356impl Validate for SetCrossfadeModeOperationRequest {}
357
358define_upnp_operation! {
359 operation: SetPlayModeOperation,
360 action: "SetPlayMode",
361 service: AVTransport,
362 request: {
363 new_play_mode: String,
364 },
365 response: (),
366 payload: |req| {
367 format!(
368 "<InstanceID>{}</InstanceID><NewPlayMode>{}</NewPlayMode>",
369 req.instance_id,
370 crate::operation::xml_escape(&req.new_play_mode)
371 )
372 },
373 parse: |_xml| Ok(()),
374}
375
376impl Validate for SetPlayModeOperationRequest {
377 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
378 match self.new_play_mode.as_str() {
379 "NORMAL" | "REPEAT_ALL" | "REPEAT_ONE" | "SHUFFLE_NOREPEAT" | "SHUFFLE"
380 | "SHUFFLE_REPEAT_ONE" => Ok(()),
381 other => Err(crate::operation::ValidationError::Custom {
382 parameter: "new_play_mode".to_string(),
383 message: format!(
384 "Invalid play mode '{other}'. Must be NORMAL, REPEAT_ALL, REPEAT_ONE, SHUFFLE_NOREPEAT, SHUFFLE, or SHUFFLE_REPEAT_ONE"
385 ),
386 }),
387 }
388 }
389}
390
391define_upnp_operation! {
396 operation: ConfigureSleepTimerOperation,
397 action: "ConfigureSleepTimer",
398 service: AVTransport,
399 request: {
400 new_sleep_timer_duration: String,
401 },
402 response: (),
403 payload: |req| {
404 format!(
405 "<InstanceID>{}</InstanceID><NewSleepTimerDuration>{}</NewSleepTimerDuration>",
406 req.instance_id,
407 crate::operation::xml_escape(&req.new_sleep_timer_duration)
408 )
409 },
410 parse: |_xml| Ok(()),
411}
412
413impl Validate for ConfigureSleepTimerOperationRequest {}
414
415define_operation_with_response! {
416 operation: GetRemainingSleepTimerDurationOperation,
417 action: "GetRemainingSleepTimerDuration",
418 service: AVTransport,
419 request: {},
420 response: GetRemainingSleepTimerDurationResponse {
421 remaining_sleep_timer_duration: String,
422 current_sleep_timer_generation: u32,
423 },
424 xml_mapping: {
425 remaining_sleep_timer_duration: "RemainingSleepTimerDuration",
426 current_sleep_timer_generation: "CurrentSleepTimerGeneration",
427 },
428}
429
430impl Validate for GetRemainingSleepTimerDurationOperationRequest {}
431
432use serde::{Deserialize, Serialize};
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct AddURIToQueueOperationRequest {
442 pub instance_id: u32,
443 pub enqueued_uri: String,
444 pub enqueued_uri_meta_data: String,
445 pub desired_first_track_number_enqueued: u32,
446 pub enqueue_as_next: bool,
447}
448
449impl Validate for AddURIToQueueOperationRequest {}
450
451#[derive(Debug, Clone, Serialize, Deserialize, Default)]
452pub struct AddURIToQueueResponse {
453 pub first_track_number_enqueued: u32,
454 pub num_tracks_added: u32,
455 pub new_queue_length: u32,
456}
457
458pub struct AddURIToQueueOperation;
459
460impl crate::operation::UPnPOperation for AddURIToQueueOperation {
461 type Request = AddURIToQueueOperationRequest;
462 type Response = AddURIToQueueResponse;
463
464 const SERVICE: crate::service::Service = crate::service::Service::AVTransport;
465 const ACTION: &'static str = "AddURIToQueue";
466
467 fn build_payload(request: &Self::Request) -> Result<String, crate::operation::ValidationError> {
468 <Self::Request as Validate>::validate(request, crate::operation::ValidationLevel::Basic)?;
469 Ok(format!(
470 "<InstanceID>{}</InstanceID><EnqueuedURI>{}</EnqueuedURI><EnqueuedURIMetaData>{}</EnqueuedURIMetaData><DesiredFirstTrackNumberEnqueued>{}</DesiredFirstTrackNumberEnqueued><EnqueueAsNext>{}</EnqueueAsNext>",
471 request.instance_id,
472 request.enqueued_uri,
473 request.enqueued_uri_meta_data,
474 request.desired_first_track_number_enqueued,
475 if request.enqueue_as_next { "1" } else { "0" }
476 ))
477 }
478
479 fn parse_response(xml: &xmltree::Element) -> Result<Self::Response, crate::error::ApiError> {
480 Ok(AddURIToQueueResponse {
481 first_track_number_enqueued: xml
482 .get_child("FirstTrackNumberEnqueued")
483 .and_then(|e| e.get_text())
484 .and_then(|s| s.parse().ok())
485 .unwrap_or_default(),
486 num_tracks_added: xml
487 .get_child("NumTracksAdded")
488 .and_then(|e| e.get_text())
489 .and_then(|s| s.parse().ok())
490 .unwrap_or_default(),
491 new_queue_length: xml
492 .get_child("NewQueueLength")
493 .and_then(|e| e.get_text())
494 .and_then(|s| s.parse().ok())
495 .unwrap_or_default(),
496 })
497 }
498}
499
500pub fn add_uri_to_queue_operation(
501 enqueued_uri: String,
502 enqueued_uri_meta_data: String,
503 desired_first_track_number_enqueued: u32,
504 enqueue_as_next: bool,
505) -> crate::operation::OperationBuilder<AddURIToQueueOperation> {
506 let request = AddURIToQueueOperationRequest {
507 instance_id: 0,
508 enqueued_uri,
509 enqueued_uri_meta_data,
510 desired_first_track_number_enqueued,
511 enqueue_as_next,
512 };
513 crate::operation::OperationBuilder::new(request)
514}
515
516define_upnp_operation! {
517 operation: RemoveTrackFromQueueOperation,
518 action: "RemoveTrackFromQueue",
519 service: AVTransport,
520 request: {
521 object_id: String,
522 update_id: u32,
523 },
524 response: (),
525 payload: |req| {
526 format!(
527 "<InstanceID>{}</InstanceID><ObjectID>{}</ObjectID><UpdateID>{}</UpdateID>",
528 req.instance_id,
529 crate::operation::xml_escape(&req.object_id),
530 req.update_id
531 )
532 },
533 parse: |_xml| Ok(()),
534}
535
536impl Validate for RemoveTrackFromQueueOperationRequest {}
537
538define_operation_with_response! {
539 operation: RemoveTrackRangeFromQueueOperation,
540 action: "RemoveTrackRangeFromQueue",
541 service: AVTransport,
542 request: {
543 update_id: u32,
544 starting_index: u32,
545 number_of_tracks: u32,
546 },
547 response: RemoveTrackRangeFromQueueResponse {
548 new_update_id: u32,
549 },
550 xml_mapping: {
551 new_update_id: "NewUpdateID",
552 },
553}
554
555impl Validate for RemoveTrackRangeFromQueueOperationRequest {}
556
557define_upnp_operation! {
558 operation: RemoveAllTracksFromQueueOperation,
559 action: "RemoveAllTracksFromQueue",
560 service: AVTransport,
561 request: {},
562 response: (),
563 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
564 parse: |_xml| Ok(()),
565}
566
567impl Validate for RemoveAllTracksFromQueueOperationRequest {}
568
569define_operation_with_response! {
570 operation: SaveQueueOperation,
571 action: "SaveQueue",
572 service: AVTransport,
573 request: {
574 title: String,
575 object_id: String,
576 },
577 response: SaveQueueResponse {
578 assigned_object_id: String,
579 },
580 xml_mapping: {
581 assigned_object_id: "AssignedObjectID",
582 },
583}
584
585impl Validate for SaveQueueOperationRequest {}
586
587define_operation_with_response! {
588 operation: CreateSavedQueueOperation,
589 action: "CreateSavedQueue",
590 service: AVTransport,
591 request: {
592 title: String,
593 enqueued_uri: String,
594 enqueued_uri_meta_data: String,
595 },
596 response: CreateSavedQueueResponse {
597 num_tracks_added: u32,
598 new_queue_length: u32,
599 assigned_object_id: String,
600 new_update_id: u32,
601 },
602 xml_mapping: {
603 num_tracks_added: "NumTracksAdded",
604 new_queue_length: "NewQueueLength",
605 assigned_object_id: "AssignedObjectID",
606 new_update_id: "NewUpdateID",
607 },
608}
609
610impl Validate for CreateSavedQueueOperationRequest {}
611
612define_upnp_operation! {
613 operation: BackupQueueOperation,
614 action: "BackupQueue",
615 service: AVTransport,
616 request: {},
617 response: (),
618 payload: |req| format!("<InstanceID>{}</InstanceID>", req.instance_id),
619 parse: |_xml| Ok(()),
620}
621
622impl Validate for BackupQueueOperationRequest {}
623
624define_operation_with_response! {
629 operation: BecomeCoordinatorOfStandaloneGroupOperation,
630 action: "BecomeCoordinatorOfStandaloneGroup",
631 service: AVTransport,
632 request: {},
633 response: BecomeCoordinatorOfStandaloneGroupResponse {
634 delegated_group_coordinator_id: String,
635 new_group_id: String,
636 },
637 xml_mapping: {
638 delegated_group_coordinator_id: "DelegatedGroupCoordinatorID",
639 new_group_id: "NewGroupID",
640 },
641}
642
643impl Validate for BecomeCoordinatorOfStandaloneGroupOperationRequest {}
644
645define_upnp_operation! {
646 operation: DelegateGroupCoordinationToOperation,
647 action: "DelegateGroupCoordinationTo",
648 service: AVTransport,
649 request: {
650 new_coordinator: String,
651 rejoin_group: bool,
652 },
653 response: (),
654 payload: |req| {
655 format!(
656 "<InstanceID>{}</InstanceID><NewCoordinator>{}</NewCoordinator><RejoinGroup>{}</RejoinGroup>",
657 req.instance_id,
658 crate::operation::xml_escape(&req.new_coordinator),
659 if req.rejoin_group { "true" } else { "false" }
660 )
661 },
662 parse: |_xml| Ok(()),
663}
664
665impl Validate for DelegateGroupCoordinationToOperationRequest {}
666
667define_upnp_operation! {
672 operation: SnoozeAlarmOperation,
673 action: "SnoozeAlarm",
674 service: AVTransport,
675 request: {
676 duration: String,
677 },
678 response: (),
679 payload: |req| {
680 format!(
681 "<InstanceID>{}</InstanceID><Duration>{}</Duration>",
682 req.instance_id,
683 crate::operation::xml_escape(&req.duration)
684 )
685 },
686 parse: |_xml| Ok(()),
687}
688
689impl Validate for SnoozeAlarmOperationRequest {}
690
691define_operation_with_response! {
692 operation: GetRunningAlarmPropertiesOperation,
693 action: "GetRunningAlarmProperties",
694 service: AVTransport,
695 request: {},
696 response: GetRunningAlarmPropertiesResponse {
697 alarm_id: u32,
698 group_id: String,
699 logged_start_time: String,
700 },
701 xml_mapping: {
702 alarm_id: "AlarmID",
703 group_id: "GroupID",
704 logged_start_time: "LoggedStartTime",
705 },
706}
707
708impl Validate for GetRunningAlarmPropertiesOperationRequest {}
709
710pub use next_operation as next;
716pub use pause_operation as pause;
717pub use play_operation as play;
718pub use previous_operation as previous;
719pub use stop_operation as stop;
720
721pub use get_position_info_operation as get_position_info;
723pub use seek_operation as seek;
724
725pub use get_current_transport_actions_operation as get_current_transport_actions;
727pub use get_device_capabilities_operation as get_device_capabilities;
728pub use get_transport_info_operation as get_transport_info;
729pub use get_transport_settings_operation as get_transport_settings;
730
731pub use get_media_info_operation as get_media_info;
733pub use set_a_v_transport_u_r_i_operation as set_av_transport_uri;
734pub use set_next_a_v_transport_u_r_i_operation as set_next_av_transport_uri;
735
736pub use get_crossfade_mode_operation as get_crossfade_mode;
738pub use set_crossfade_mode_operation as set_crossfade_mode;
739pub use set_play_mode_operation as set_play_mode;
740
741pub use configure_sleep_timer_operation as configure_sleep_timer;
743pub use get_remaining_sleep_timer_duration_operation as get_remaining_sleep_timer_duration;
744
745pub use add_uri_to_queue_operation as add_uri_to_queue;
747pub use backup_queue_operation as backup_queue;
748pub use create_saved_queue_operation as create_saved_queue;
749pub use remove_all_tracks_from_queue_operation as remove_all_tracks_from_queue;
750pub use remove_track_from_queue_operation as remove_track_from_queue;
751pub use remove_track_range_from_queue_operation as remove_track_range_from_queue;
752pub use save_queue_operation as save_queue;
753
754pub use become_coordinator_of_standalone_group_operation as become_coordinator_of_standalone_group;
756pub use delegate_group_coordination_to_operation as delegate_group_coordination_to;
757
758pub use get_running_alarm_properties_operation as get_running_alarm_properties;
760pub use snooze_alarm_operation as snooze_alarm;
761
762pub const SERVICE: crate::Service = crate::Service::AVTransport;
768
769pub fn subscribe(
771 client: &crate::SonosClient,
772 ip: &str,
773 callback_url: &str,
774) -> crate::Result<crate::ManagedSubscription> {
775 client.subscribe(ip, SERVICE, callback_url)
776}
777
778pub fn subscribe_with_timeout(
780 client: &crate::SonosClient,
781 ip: &str,
782 callback_url: &str,
783 timeout_seconds: u32,
784) -> crate::Result<crate::ManagedSubscription> {
785 client.subscribe_with_timeout(ip, SERVICE, callback_url, timeout_seconds)
786}
787
788#[cfg(test)]
793mod tests {
794 use super::*;
795 use crate::operation::UPnPOperation;
796
797 #[test]
800 fn test_play_operation_builder() {
801 let op = play_operation("1".to_string()).build().unwrap();
802 assert_eq!(op.request().speed, "1");
803 assert_eq!(op.metadata().action, "Play");
804 }
805
806 #[test]
807 fn test_play_validation() {
808 let request = PlayOperationRequest {
809 instance_id: 0,
810 speed: "".to_string(),
811 };
812 assert!(request.validate_basic().is_err());
813
814 let request = PlayOperationRequest {
815 instance_id: 0,
816 speed: "1".to_string(),
817 };
818 assert!(request.validate_basic().is_ok());
819 }
820
821 #[test]
822 fn test_play_payload() {
823 let request = PlayOperationRequest {
824 instance_id: 0,
825 speed: "1".to_string(),
826 };
827 let payload = PlayOperation::build_payload(&request).unwrap();
828 assert!(payload.contains("<InstanceID>0</InstanceID>"));
829 assert!(payload.contains("<Speed>1</Speed>"));
830 }
831
832 #[test]
833 fn test_pause_operation_builder() {
834 let op = pause_operation().build().unwrap();
835 assert_eq!(op.metadata().action, "Pause");
836 }
837
838 #[test]
839 fn test_stop_operation_builder() {
840 let op = stop_operation().build().unwrap();
841 assert_eq!(op.metadata().action, "Stop");
842 }
843
844 #[test]
845 fn test_next_operation_builder() {
846 let op = next_operation().build().unwrap();
847 assert_eq!(op.metadata().action, "Next");
848 }
849
850 #[test]
851 fn test_previous_operation_builder() {
852 let op = previous_operation().build().unwrap();
853 assert_eq!(op.metadata().action, "Previous");
854 }
855
856 #[test]
859 fn test_seek_operation_builder() {
860 let op = seek_operation("TRACK_NR".to_string(), "5".to_string())
861 .build()
862 .unwrap();
863 assert_eq!(op.request().unit, "TRACK_NR");
864 assert_eq!(op.request().target, "5");
865 assert_eq!(op.metadata().action, "Seek");
866 }
867
868 #[test]
869 fn test_seek_validation() {
870 let request = SeekOperationRequest {
871 instance_id: 0,
872 unit: "INVALID".to_string(),
873 target: "5".to_string(),
874 };
875 assert!(request.validate_basic().is_err());
876
877 let request = SeekOperationRequest {
878 instance_id: 0,
879 unit: "REL_TIME".to_string(),
880 target: "0:01:30".to_string(),
881 };
882 assert!(request.validate_basic().is_ok());
883 }
884
885 #[test]
886 fn test_seek_payload() {
887 let request = SeekOperationRequest {
888 instance_id: 0,
889 unit: "TRACK_NR".to_string(),
890 target: "3".to_string(),
891 };
892 let payload = SeekOperation::build_payload(&request).unwrap();
893 assert!(payload.contains("<Unit>TRACK_NR</Unit>"));
894 assert!(payload.contains("<Target>3</Target>"));
895 }
896
897 #[test]
900 fn test_get_transport_info_builder() {
901 let op = get_transport_info_operation().build().unwrap();
902 assert_eq!(op.metadata().action, "GetTransportInfo");
903 }
904
905 #[test]
906 fn test_get_position_info_builder() {
907 let op = get_position_info_operation().build().unwrap();
908 assert_eq!(op.metadata().action, "GetPositionInfo");
909 }
910
911 #[test]
912 fn test_get_media_info_builder() {
913 let op = get_media_info_operation().build().unwrap();
914 assert_eq!(op.metadata().action, "GetMediaInfo");
915 }
916
917 #[test]
918 fn test_get_transport_settings_builder() {
919 let op = get_transport_settings_operation().build().unwrap();
920 assert_eq!(op.metadata().action, "GetTransportSettings");
921 }
922
923 #[test]
926 fn test_get_crossfade_mode_builder() {
927 let op = get_crossfade_mode_operation().build().unwrap();
928 assert_eq!(op.metadata().action, "GetCrossfadeMode");
929 }
930
931 #[test]
932 fn test_set_crossfade_mode_builder() {
933 let op = set_crossfade_mode_operation(true).build().unwrap();
934 assert!(op.request().crossfade_mode);
935 assert_eq!(op.metadata().action, "SetCrossfadeMode");
936 }
937
938 #[test]
939 fn test_set_crossfade_mode_payload() {
940 let request = SetCrossfadeModeOperationRequest {
941 instance_id: 0,
942 crossfade_mode: true,
943 };
944 let payload = SetCrossfadeModeOperation::build_payload(&request).unwrap();
945 assert!(payload.contains("<CrossfadeMode>1</CrossfadeMode>"));
946
947 let request = SetCrossfadeModeOperationRequest {
948 instance_id: 0,
949 crossfade_mode: false,
950 };
951 let payload = SetCrossfadeModeOperation::build_payload(&request).unwrap();
952 assert!(payload.contains("<CrossfadeMode>0</CrossfadeMode>"));
953 }
954
955 #[test]
956 fn test_set_play_mode_builder() {
957 let op = set_play_mode_operation("SHUFFLE".to_string())
958 .build()
959 .unwrap();
960 assert_eq!(op.request().new_play_mode, "SHUFFLE");
961 assert_eq!(op.metadata().action, "SetPlayMode");
962 }
963
964 #[test]
965 fn test_set_play_mode_validation() {
966 let request = SetPlayModeOperationRequest {
967 instance_id: 0,
968 new_play_mode: "INVALID".to_string(),
969 };
970 assert!(request.validate_basic().is_err());
971
972 let request = SetPlayModeOperationRequest {
973 instance_id: 0,
974 new_play_mode: "REPEAT_ALL".to_string(),
975 };
976 assert!(request.validate_basic().is_ok());
977 }
978
979 #[test]
982 fn test_configure_sleep_timer_builder() {
983 let op = configure_sleep_timer_operation("0:30:00".to_string())
984 .build()
985 .unwrap();
986 assert_eq!(op.request().new_sleep_timer_duration, "0:30:00");
987 assert_eq!(op.metadata().action, "ConfigureSleepTimer");
988 }
989
990 #[test]
991 fn test_get_remaining_sleep_timer_duration_builder() {
992 let op = get_remaining_sleep_timer_duration_operation()
993 .build()
994 .unwrap();
995 assert_eq!(op.metadata().action, "GetRemainingSleepTimerDuration");
996 }
997
998 #[test]
1001 fn test_remove_all_tracks_from_queue_builder() {
1002 let op = remove_all_tracks_from_queue_operation().build().unwrap();
1003 assert_eq!(op.metadata().action, "RemoveAllTracksFromQueue");
1004 }
1005
1006 #[test]
1007 fn test_backup_queue_builder() {
1008 let op = backup_queue_operation().build().unwrap();
1009 assert_eq!(op.metadata().action, "BackupQueue");
1010 }
1011
1012 #[test]
1015 fn test_become_coordinator_of_standalone_group_builder() {
1016 let op = become_coordinator_of_standalone_group_operation()
1017 .build()
1018 .unwrap();
1019 assert_eq!(op.metadata().action, "BecomeCoordinatorOfStandaloneGroup");
1020 }
1021
1022 #[test]
1025 fn test_snooze_alarm_builder() {
1026 let op = snooze_alarm_operation("0:10:00".to_string())
1027 .build()
1028 .unwrap();
1029 assert_eq!(op.request().duration, "0:10:00");
1030 assert_eq!(op.metadata().action, "SnoozeAlarm");
1031 }
1032
1033 #[test]
1034 fn test_get_running_alarm_properties_builder() {
1035 let op = get_running_alarm_properties_operation().build().unwrap();
1036 assert_eq!(op.metadata().action, "GetRunningAlarmProperties");
1037 }
1038
1039 #[test]
1042 fn test_service_constant() {
1043 assert_eq!(SERVICE, crate::Service::AVTransport);
1044 }
1045
1046 #[test]
1047 fn test_subscription_helpers() {
1048 let client = crate::SonosClient::new();
1049 let _subscribe_fn = || subscribe(&client, "192.168.1.100", "http://callback.url");
1050 let _subscribe_timeout_fn =
1051 || subscribe_with_timeout(&client, "192.168.1.100", "http://callback.url", 3600);
1052 }
1053}