1use crate::operation::{parse_sonos_bool, validate_channel};
15use crate::{define_operation_with_response, define_upnp_operation, Validate};
16use paste::paste;
17
18define_operation_with_response! {
20 operation: GetVolumeOperation,
21 action: "GetVolume",
22 service: RenderingControl,
23 request: {
24 channel: String,
25 },
26 response: GetVolumeResponse {
27 current_volume: u8,
28 },
29 xml_mapping: {
30 current_volume: "CurrentVolume",
31 },
32}
33
34impl Validate for GetVolumeOperationRequest {
36 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
37 validate_channel(&self.channel)
38 }
39}
40
41define_upnp_operation! {
43 operation: SetVolumeOperation,
44 action: "SetVolume",
45 service: RenderingControl,
46 request: {
47 channel: String,
48 desired_volume: u8,
49 },
50 response: (),
51 payload: |req| {
52 format!(
53 "<InstanceID>{}</InstanceID><Channel>{}</Channel><DesiredVolume>{}</DesiredVolume>",
54 req.instance_id, req.channel, req.desired_volume
55 )
56 },
57 parse: |_xml| Ok(()),
58}
59
60impl Validate for SetVolumeOperationRequest {
62 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
63 if self.desired_volume > 100 {
64 return Err(crate::operation::ValidationError::range_error(
65 "desired_volume",
66 0,
67 100,
68 self.desired_volume,
69 ));
70 }
71 validate_channel(&self.channel)
72 }
73}
74
75define_operation_with_response! {
77 operation: SetRelativeVolumeOperation,
78 action: "SetRelativeVolume",
79 service: RenderingControl,
80 request: {
81 channel: String,
82 adjustment: i8,
83 },
84 response: SetRelativeVolumeResponse {
85 new_volume: u8,
86 },
87 xml_mapping: {
88 new_volume: "NewVolume",
89 },
90}
91
92impl Validate for SetRelativeVolumeOperationRequest {
94 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
95 if self.adjustment < -100 || self.adjustment > 100 {
97 return Err(crate::operation::ValidationError::range_error(
98 "adjustment",
99 -100,
100 100,
101 self.adjustment,
102 ));
103 }
104 validate_channel(&self.channel)
105 }
106}
107
108#[derive(serde::Serialize, Clone, Debug, PartialEq)]
115pub struct GetMuteOperationRequest {
116 pub channel: String,
117 pub instance_id: u32,
118}
119
120#[derive(serde::Deserialize, Debug, Clone, PartialEq)]
121pub struct GetMuteResponse {
122 pub current_mute: bool,
123}
124
125pub struct GetMuteOperation;
126
127impl crate::operation::UPnPOperation for GetMuteOperation {
128 type Request = GetMuteOperationRequest;
129 type Response = GetMuteResponse;
130
131 const SERVICE: crate::service::Service = crate::service::Service::RenderingControl;
132 const ACTION: &'static str = "GetMute";
133
134 fn build_payload(request: &Self::Request) -> Result<String, crate::operation::ValidationError> {
135 request.validate(crate::operation::ValidationLevel::Basic)?;
136 Ok(format!(
137 "<InstanceID>{}</InstanceID><Channel>{}</Channel>",
138 request.instance_id, request.channel
139 ))
140 }
141
142 fn parse_response(xml: &str) -> Result<Self::Response, crate::error::ApiError> {
143 Ok(GetMuteResponse {
144 current_mute: parse_sonos_bool(xml, "CurrentMute"),
145 })
146 }
147}
148
149pub fn get_mute_operation(channel: String) -> crate::operation::OperationBuilder<GetMuteOperation> {
150 let request = GetMuteOperationRequest {
151 channel,
152 instance_id: 0,
153 };
154 crate::operation::OperationBuilder::new(request)
155}
156
157impl Validate for GetMuteOperationRequest {
158 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
159 validate_channel(&self.channel)
160 }
161}
162
163pub use get_mute_operation as get_mute;
164
165define_upnp_operation! {
170 operation: SetMuteOperation,
171 action: "SetMute",
172 service: RenderingControl,
173 request: {
174 channel: String,
175 desired_mute: bool,
176 },
177 response: (),
178 payload: |req| {
179 format!(
180 "<InstanceID>{}</InstanceID><Channel>{}</Channel><DesiredMute>{}</DesiredMute>",
181 req.instance_id, req.channel, if req.desired_mute { "1" } else { "0" }
182 )
183 },
184 parse: |_xml| Ok(()),
185}
186
187impl Validate for SetMuteOperationRequest {
188 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
189 validate_channel(&self.channel)
190 }
191}
192
193pub use set_mute_operation as set_mute;
194
195define_operation_with_response! {
200 operation: GetBassOperation,
201 action: "GetBass",
202 service: RenderingControl,
203 request: {},
204 response: GetBassResponse {
205 current_bass: i8,
206 },
207 xml_mapping: {
208 current_bass: "CurrentBass",
209 },
210}
211
212impl Validate for GetBassOperationRequest {}
213
214pub use get_bass_operation as get_bass;
215
216define_upnp_operation! {
221 operation: SetBassOperation,
222 action: "SetBass",
223 service: RenderingControl,
224 request: {
225 desired_bass: i8,
226 },
227 response: (),
228 payload: |req| {
229 format!(
230 "<InstanceID>{}</InstanceID><DesiredBass>{}</DesiredBass>",
231 req.instance_id, req.desired_bass
232 )
233 },
234 parse: |_xml| Ok(()),
235}
236
237impl Validate for SetBassOperationRequest {
238 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
239 if self.desired_bass < -10 || self.desired_bass > 10 {
240 return Err(crate::operation::ValidationError::range_error(
241 "desired_bass",
242 -10,
243 10,
244 self.desired_bass,
245 ));
246 }
247 Ok(())
248 }
249}
250
251pub use set_bass_operation as set_bass;
252
253define_operation_with_response! {
258 operation: GetTrebleOperation,
259 action: "GetTreble",
260 service: RenderingControl,
261 request: {},
262 response: GetTrebleResponse {
263 current_treble: i8,
264 },
265 xml_mapping: {
266 current_treble: "CurrentTreble",
267 },
268}
269
270impl Validate for GetTrebleOperationRequest {}
271
272pub use get_treble_operation as get_treble;
273
274define_upnp_operation! {
279 operation: SetTrebleOperation,
280 action: "SetTreble",
281 service: RenderingControl,
282 request: {
283 desired_treble: i8,
284 },
285 response: (),
286 payload: |req| {
287 format!(
288 "<InstanceID>{}</InstanceID><DesiredTreble>{}</DesiredTreble>",
289 req.instance_id, req.desired_treble
290 )
291 },
292 parse: |_xml| Ok(()),
293}
294
295impl Validate for SetTrebleOperationRequest {
296 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
297 if self.desired_treble < -10 || self.desired_treble > 10 {
298 return Err(crate::operation::ValidationError::range_error(
299 "desired_treble",
300 -10,
301 10,
302 self.desired_treble,
303 ));
304 }
305 Ok(())
306 }
307}
308
309pub use set_treble_operation as set_treble;
310
311#[derive(serde::Serialize, Clone, Debug, PartialEq)]
317pub struct GetLoudnessOperationRequest {
318 pub channel: String,
319 pub instance_id: u32,
320}
321
322#[derive(serde::Deserialize, Debug, Clone, PartialEq)]
323pub struct GetLoudnessResponse {
324 pub current_loudness: bool,
325}
326
327pub struct GetLoudnessOperation;
328
329impl crate::operation::UPnPOperation for GetLoudnessOperation {
330 type Request = GetLoudnessOperationRequest;
331 type Response = GetLoudnessResponse;
332
333 const SERVICE: crate::service::Service = crate::service::Service::RenderingControl;
334 const ACTION: &'static str = "GetLoudness";
335
336 fn build_payload(request: &Self::Request) -> Result<String, crate::operation::ValidationError> {
337 request.validate(crate::operation::ValidationLevel::Basic)?;
338 Ok(format!(
339 "<InstanceID>{}</InstanceID><Channel>{}</Channel>",
340 request.instance_id, request.channel
341 ))
342 }
343
344 fn parse_response(xml: &str) -> Result<Self::Response, crate::error::ApiError> {
345 Ok(GetLoudnessResponse {
346 current_loudness: parse_sonos_bool(xml, "CurrentLoudness"),
347 })
348 }
349}
350
351pub fn get_loudness_operation(
352 channel: String,
353) -> crate::operation::OperationBuilder<GetLoudnessOperation> {
354 let request = GetLoudnessOperationRequest {
355 channel,
356 instance_id: 0,
357 };
358 crate::operation::OperationBuilder::new(request)
359}
360
361impl Validate for GetLoudnessOperationRequest {
362 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
363 validate_channel(&self.channel)
364 }
365}
366
367pub use get_loudness_operation as get_loudness;
368
369define_upnp_operation! {
374 operation: SetLoudnessOperation,
375 action: "SetLoudness",
376 service: RenderingControl,
377 request: {
378 channel: String,
379 desired_loudness: bool,
380 },
381 response: (),
382 payload: |req| {
383 format!(
384 "<InstanceID>{}</InstanceID><Channel>{}</Channel><DesiredLoudness>{}</DesiredLoudness>",
385 req.instance_id, req.channel, if req.desired_loudness { "1" } else { "0" }
386 )
387 },
388 parse: |_xml| Ok(()),
389}
390
391impl Validate for SetLoudnessOperationRequest {
392 fn validate_basic(&self) -> Result<(), crate::operation::ValidationError> {
393 validate_channel(&self.channel)
394 }
395}
396
397pub use set_loudness_operation as set_loudness;
398
399pub use get_volume_operation as get_volume;
401pub use set_relative_volume_operation as set_relative_volume;
402pub use set_volume_operation as set_volume;
403
404pub const SERVICE: crate::Service = crate::Service::RenderingControl;
406
407pub fn subscribe(
437 client: &crate::SonosClient,
438 ip: &str,
439 callback_url: &str,
440) -> crate::Result<crate::ManagedSubscription> {
441 client.subscribe(ip, SERVICE, callback_url)
442}
443
444pub fn subscribe_with_timeout(
457 client: &crate::SonosClient,
458 ip: &str,
459 callback_url: &str,
460 timeout_seconds: u32,
461) -> crate::Result<crate::ManagedSubscription> {
462 client.subscribe_with_timeout(ip, SERVICE, callback_url, timeout_seconds)
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::operation::UPnPOperation;
469
470 #[test]
471 fn test_volume_operations() {
472 let get_vol = get_volume_operation("Master".to_string()).build().unwrap();
473 assert_eq!(get_vol.request().channel, "Master");
474
475 let set_vol = set_volume_operation("Master".to_string(), 75)
476 .build()
477 .unwrap();
478 assert_eq!(set_vol.request().desired_volume, 75);
479 }
480
481 #[test]
482 fn test_volume_validation() {
483 let request = SetVolumeOperationRequest {
484 instance_id: 0,
485 channel: "Master".to_string(),
486 desired_volume: 150, };
488 assert!(request.validate_basic().is_err());
489
490 let request = SetVolumeOperationRequest {
491 instance_id: 0,
492 channel: "Invalid".to_string(), desired_volume: 50,
494 };
495 assert!(request.validate_basic().is_err());
496 }
497
498 #[test]
499 fn test_relative_volume_validation() {
500 let request = SetRelativeVolumeOperationRequest {
501 instance_id: 0,
502 channel: "Master".to_string(),
503 adjustment: 100, };
505 assert!(request.validate_basic().is_ok());
506
507 let request = SetRelativeVolumeOperationRequest {
509 instance_id: 0,
510 channel: "Invalid".to_string(),
511 adjustment: 10,
512 };
513 assert!(request.validate_basic().is_err());
514 }
515
516 #[test]
517 fn test_service_constant() {
518 assert_eq!(SERVICE, crate::Service::RenderingControl);
520 }
521
522 #[test]
523 fn test_get_volume_payload() {
524 let request = GetVolumeOperationRequest {
525 instance_id: 0,
526 channel: "Master".to_string(),
527 };
528 let payload = GetVolumeOperation::build_payload(&request).unwrap();
529
530 assert!(payload.contains("<InstanceID>0</InstanceID>"));
532 assert!(payload.contains("<Channel>Master</Channel>"));
533 assert_eq!(
534 payload,
535 "<InstanceID>0</InstanceID><Channel>Master</Channel>"
536 );
537 }
538
539 #[test]
543 fn test_set_relative_volume_payload_element_names() {
544 let request = SetRelativeVolumeOperationRequest {
545 instance_id: 0,
546 channel: "Master".to_string(),
547 adjustment: -5,
548 };
549 let payload = SetRelativeVolumeOperation::build_payload(&request).unwrap();
550 assert_eq!(
551 payload,
552 "<InstanceID>0</InstanceID><Channel>Master</Channel><Adjustment>-5</Adjustment>"
553 );
554 }
555
556 #[test]
557 fn test_service_level_subscription_helpers() {
558 let client = crate::SonosClient::new();
560
561 let _subscribe_fn = || subscribe(&client, "192.168.1.100", "http://callback.url");
563
564 let _subscribe_timeout_fn =
566 || subscribe_with_timeout(&client, "192.168.1.100", "http://callback.url", 3600);
567 }
568
569 #[test]
570 fn test_subscription_uses_correct_service() {
571 let _client = crate::SonosClient::new();
573
574 assert_eq!(SERVICE, crate::Service::RenderingControl);
576
577 }
581
582 #[test]
587 fn test_get_mute_builder() {
588 let op = get_mute_operation("Master".to_string()).build().unwrap();
589 assert_eq!(op.request().channel, "Master");
590 assert_eq!(op.request().instance_id, 0);
591 }
592
593 #[test]
594 fn test_get_mute_payload() {
595 let request = GetMuteOperationRequest {
596 instance_id: 0,
597 channel: "Master".to_string(),
598 };
599 let payload = GetMuteOperation::build_payload(&request).unwrap();
600 assert_eq!(
601 payload,
602 "<InstanceID>0</InstanceID><Channel>Master</Channel>"
603 );
604 }
605
606 #[test]
607 fn test_get_mute_parse_response_true() {
608 let xml_str = r#"<GetMuteResponse><CurrentMute>1</CurrentMute></GetMuteResponse>"#;
609 let response = GetMuteOperation::parse_response(xml_str).unwrap();
610 assert!(response.current_mute);
611 }
612
613 #[test]
614 fn test_get_mute_parse_response_false() {
615 let xml_str = r#"<GetMuteResponse><CurrentMute>0</CurrentMute></GetMuteResponse>"#;
616 let response = GetMuteOperation::parse_response(xml_str).unwrap();
617 assert!(!response.current_mute);
618 }
619
620 #[test]
621 fn test_get_mute_rejects_invalid_channel() {
622 let request = GetMuteOperationRequest {
623 instance_id: 0,
624 channel: "Invalid".to_string(),
625 };
626 assert!(request.validate_basic().is_err());
627 }
628
629 #[test]
630 fn test_set_mute_builder() {
631 let op = set_mute_operation("Master".to_string(), true)
632 .build()
633 .unwrap();
634 assert!(op.request().desired_mute);
635 assert_eq!(op.request().channel, "Master");
636 }
637
638 #[test]
639 fn test_set_mute_payload_true() {
640 let request = SetMuteOperationRequest {
641 instance_id: 0,
642 channel: "Master".to_string(),
643 desired_mute: true,
644 };
645 let payload = SetMuteOperation::build_payload(&request).unwrap();
646 assert!(payload.contains("<DesiredMute>1</DesiredMute>"));
647 }
648
649 #[test]
650 fn test_set_mute_payload_false() {
651 let request = SetMuteOperationRequest {
652 instance_id: 0,
653 channel: "Master".to_string(),
654 desired_mute: false,
655 };
656 let payload = SetMuteOperation::build_payload(&request).unwrap();
657 assert!(payload.contains("<DesiredMute>0</DesiredMute>"));
658 }
659
660 #[test]
661 fn test_set_mute_rejects_invalid_channel() {
662 let request = SetMuteOperationRequest {
663 instance_id: 0,
664 channel: "Invalid".to_string(),
665 desired_mute: true,
666 };
667 assert!(request.validate_basic().is_err());
668 }
669
670 #[test]
675 fn test_get_bass_builder() {
676 let op = get_bass_operation().build().unwrap();
677 assert_eq!(op.request().instance_id, 0);
678 }
679
680 #[test]
681 fn test_get_bass_payload() {
682 let request = GetBassOperationRequest { instance_id: 0 };
683 let payload = GetBassOperation::build_payload(&request).unwrap();
684 assert_eq!(payload, "<InstanceID>0</InstanceID>");
685 }
686
687 #[test]
688 fn test_set_bass_builder() {
689 let op = set_bass_operation(5).build().unwrap();
690 assert_eq!(op.request().desired_bass, 5);
691 }
692
693 #[test]
694 fn test_set_bass_payload() {
695 let request = SetBassOperationRequest {
696 instance_id: 0,
697 desired_bass: -5,
698 };
699 let payload = SetBassOperation::build_payload(&request).unwrap();
700 assert!(payload.contains("<DesiredBass>-5</DesiredBass>"));
701 }
702
703 #[test]
704 fn test_set_bass_validation() {
705 let request = SetBassOperationRequest {
707 instance_id: 0,
708 desired_bass: -10,
709 };
710 assert!(request.validate_basic().is_ok());
711 let request = SetBassOperationRequest {
712 instance_id: 0,
713 desired_bass: 10,
714 };
715 assert!(request.validate_basic().is_ok());
716
717 let request = SetBassOperationRequest {
719 instance_id: 0,
720 desired_bass: -11,
721 };
722 assert!(request.validate_basic().is_err());
723 let request = SetBassOperationRequest {
724 instance_id: 0,
725 desired_bass: 11,
726 };
727 assert!(request.validate_basic().is_err());
728 }
729
730 #[test]
735 fn test_get_treble_builder() {
736 let op = get_treble_operation().build().unwrap();
737 assert_eq!(op.request().instance_id, 0);
738 }
739
740 #[test]
741 fn test_get_treble_payload() {
742 let request = GetTrebleOperationRequest { instance_id: 0 };
743 let payload = GetTrebleOperation::build_payload(&request).unwrap();
744 assert_eq!(payload, "<InstanceID>0</InstanceID>");
745 }
746
747 #[test]
748 fn test_set_treble_builder() {
749 let op = set_treble_operation(-3).build().unwrap();
750 assert_eq!(op.request().desired_treble, -3);
751 }
752
753 #[test]
754 fn test_set_treble_payload() {
755 let request = SetTrebleOperationRequest {
756 instance_id: 0,
757 desired_treble: 7,
758 };
759 let payload = SetTrebleOperation::build_payload(&request).unwrap();
760 assert!(payload.contains("<DesiredTreble>7</DesiredTreble>"));
761 }
762
763 #[test]
764 fn test_set_treble_validation() {
765 let request = SetTrebleOperationRequest {
766 instance_id: 0,
767 desired_treble: -10,
768 };
769 assert!(request.validate_basic().is_ok());
770 let request = SetTrebleOperationRequest {
771 instance_id: 0,
772 desired_treble: 10,
773 };
774 assert!(request.validate_basic().is_ok());
775
776 let request = SetTrebleOperationRequest {
777 instance_id: 0,
778 desired_treble: -11,
779 };
780 assert!(request.validate_basic().is_err());
781 let request = SetTrebleOperationRequest {
782 instance_id: 0,
783 desired_treble: 11,
784 };
785 assert!(request.validate_basic().is_err());
786 }
787
788 #[test]
793 fn test_get_loudness_builder() {
794 let op = get_loudness_operation("Master".to_string())
795 .build()
796 .unwrap();
797 assert_eq!(op.request().channel, "Master");
798 assert_eq!(op.request().instance_id, 0);
799 }
800
801 #[test]
802 fn test_get_loudness_payload() {
803 let request = GetLoudnessOperationRequest {
804 instance_id: 0,
805 channel: "Master".to_string(),
806 };
807 let payload = GetLoudnessOperation::build_payload(&request).unwrap();
808 assert_eq!(
809 payload,
810 "<InstanceID>0</InstanceID><Channel>Master</Channel>"
811 );
812 }
813
814 #[test]
815 fn test_get_loudness_parse_response_true() {
816 let xml_str =
817 r#"<GetLoudnessResponse><CurrentLoudness>1</CurrentLoudness></GetLoudnessResponse>"#;
818 let response = GetLoudnessOperation::parse_response(xml_str).unwrap();
819 assert!(response.current_loudness);
820 }
821
822 #[test]
823 fn test_get_loudness_parse_response_false() {
824 let xml_str =
825 r#"<GetLoudnessResponse><CurrentLoudness>0</CurrentLoudness></GetLoudnessResponse>"#;
826 let response = GetLoudnessOperation::parse_response(xml_str).unwrap();
827 assert!(!response.current_loudness);
828 }
829
830 #[test]
831 fn test_get_loudness_rejects_invalid_channel() {
832 let request = GetLoudnessOperationRequest {
833 instance_id: 0,
834 channel: "Invalid".to_string(),
835 };
836 assert!(request.validate_basic().is_err());
837 }
838
839 #[test]
840 fn test_set_loudness_builder() {
841 let op = set_loudness_operation("Master".to_string(), true)
842 .build()
843 .unwrap();
844 assert!(op.request().desired_loudness);
845 assert_eq!(op.request().channel, "Master");
846 }
847
848 #[test]
849 fn test_set_loudness_payload_true() {
850 let request = SetLoudnessOperationRequest {
851 instance_id: 0,
852 channel: "Master".to_string(),
853 desired_loudness: true,
854 };
855 let payload = SetLoudnessOperation::build_payload(&request).unwrap();
856 assert!(payload.contains("<DesiredLoudness>1</DesiredLoudness>"));
857 }
858
859 #[test]
860 fn test_set_loudness_payload_false() {
861 let request = SetLoudnessOperationRequest {
862 instance_id: 0,
863 channel: "Master".to_string(),
864 desired_loudness: false,
865 };
866 let payload = SetLoudnessOperation::build_payload(&request).unwrap();
867 assert!(payload.contains("<DesiredLoudness>0</DesiredLoudness>"));
868 }
869
870 #[test]
871 fn test_set_loudness_rejects_invalid_channel() {
872 let request = SetLoudnessOperationRequest {
873 instance_id: 0,
874 channel: "Invalid".to_string(),
875 desired_loudness: true,
876 };
877 assert!(request.validate_basic().is_err());
878 }
879}