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: &xmltree::Element) -> 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: &xmltree::Element) -> 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 xml = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
610 let response = GetMuteOperation::parse_response(&xml).unwrap();
611 assert!(response.current_mute);
612 }
613
614 #[test]
615 fn test_get_mute_parse_response_false() {
616 let xml_str = r#"<GetMuteResponse><CurrentMute>0</CurrentMute></GetMuteResponse>"#;
617 let xml = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
618 let response = GetMuteOperation::parse_response(&xml).unwrap();
619 assert!(!response.current_mute);
620 }
621
622 #[test]
623 fn test_get_mute_rejects_invalid_channel() {
624 let request = GetMuteOperationRequest {
625 instance_id: 0,
626 channel: "Invalid".to_string(),
627 };
628 assert!(request.validate_basic().is_err());
629 }
630
631 #[test]
632 fn test_set_mute_builder() {
633 let op = set_mute_operation("Master".to_string(), true)
634 .build()
635 .unwrap();
636 assert!(op.request().desired_mute);
637 assert_eq!(op.request().channel, "Master");
638 }
639
640 #[test]
641 fn test_set_mute_payload_true() {
642 let request = SetMuteOperationRequest {
643 instance_id: 0,
644 channel: "Master".to_string(),
645 desired_mute: true,
646 };
647 let payload = SetMuteOperation::build_payload(&request).unwrap();
648 assert!(payload.contains("<DesiredMute>1</DesiredMute>"));
649 }
650
651 #[test]
652 fn test_set_mute_payload_false() {
653 let request = SetMuteOperationRequest {
654 instance_id: 0,
655 channel: "Master".to_string(),
656 desired_mute: false,
657 };
658 let payload = SetMuteOperation::build_payload(&request).unwrap();
659 assert!(payload.contains("<DesiredMute>0</DesiredMute>"));
660 }
661
662 #[test]
663 fn test_set_mute_rejects_invalid_channel() {
664 let request = SetMuteOperationRequest {
665 instance_id: 0,
666 channel: "Invalid".to_string(),
667 desired_mute: true,
668 };
669 assert!(request.validate_basic().is_err());
670 }
671
672 #[test]
677 fn test_get_bass_builder() {
678 let op = get_bass_operation().build().unwrap();
679 assert_eq!(op.request().instance_id, 0);
680 }
681
682 #[test]
683 fn test_get_bass_payload() {
684 let request = GetBassOperationRequest { instance_id: 0 };
685 let payload = GetBassOperation::build_payload(&request).unwrap();
686 assert_eq!(payload, "<InstanceID>0</InstanceID>");
687 }
688
689 #[test]
690 fn test_set_bass_builder() {
691 let op = set_bass_operation(5).build().unwrap();
692 assert_eq!(op.request().desired_bass, 5);
693 }
694
695 #[test]
696 fn test_set_bass_payload() {
697 let request = SetBassOperationRequest {
698 instance_id: 0,
699 desired_bass: -5,
700 };
701 let payload = SetBassOperation::build_payload(&request).unwrap();
702 assert!(payload.contains("<DesiredBass>-5</DesiredBass>"));
703 }
704
705 #[test]
706 fn test_set_bass_validation() {
707 let request = SetBassOperationRequest {
709 instance_id: 0,
710 desired_bass: -10,
711 };
712 assert!(request.validate_basic().is_ok());
713 let request = SetBassOperationRequest {
714 instance_id: 0,
715 desired_bass: 10,
716 };
717 assert!(request.validate_basic().is_ok());
718
719 let request = SetBassOperationRequest {
721 instance_id: 0,
722 desired_bass: -11,
723 };
724 assert!(request.validate_basic().is_err());
725 let request = SetBassOperationRequest {
726 instance_id: 0,
727 desired_bass: 11,
728 };
729 assert!(request.validate_basic().is_err());
730 }
731
732 #[test]
737 fn test_get_treble_builder() {
738 let op = get_treble_operation().build().unwrap();
739 assert_eq!(op.request().instance_id, 0);
740 }
741
742 #[test]
743 fn test_get_treble_payload() {
744 let request = GetTrebleOperationRequest { instance_id: 0 };
745 let payload = GetTrebleOperation::build_payload(&request).unwrap();
746 assert_eq!(payload, "<InstanceID>0</InstanceID>");
747 }
748
749 #[test]
750 fn test_set_treble_builder() {
751 let op = set_treble_operation(-3).build().unwrap();
752 assert_eq!(op.request().desired_treble, -3);
753 }
754
755 #[test]
756 fn test_set_treble_payload() {
757 let request = SetTrebleOperationRequest {
758 instance_id: 0,
759 desired_treble: 7,
760 };
761 let payload = SetTrebleOperation::build_payload(&request).unwrap();
762 assert!(payload.contains("<DesiredTreble>7</DesiredTreble>"));
763 }
764
765 #[test]
766 fn test_set_treble_validation() {
767 let request = SetTrebleOperationRequest {
768 instance_id: 0,
769 desired_treble: -10,
770 };
771 assert!(request.validate_basic().is_ok());
772 let request = SetTrebleOperationRequest {
773 instance_id: 0,
774 desired_treble: 10,
775 };
776 assert!(request.validate_basic().is_ok());
777
778 let request = SetTrebleOperationRequest {
779 instance_id: 0,
780 desired_treble: -11,
781 };
782 assert!(request.validate_basic().is_err());
783 let request = SetTrebleOperationRequest {
784 instance_id: 0,
785 desired_treble: 11,
786 };
787 assert!(request.validate_basic().is_err());
788 }
789
790 #[test]
795 fn test_get_loudness_builder() {
796 let op = get_loudness_operation("Master".to_string())
797 .build()
798 .unwrap();
799 assert_eq!(op.request().channel, "Master");
800 assert_eq!(op.request().instance_id, 0);
801 }
802
803 #[test]
804 fn test_get_loudness_payload() {
805 let request = GetLoudnessOperationRequest {
806 instance_id: 0,
807 channel: "Master".to_string(),
808 };
809 let payload = GetLoudnessOperation::build_payload(&request).unwrap();
810 assert_eq!(
811 payload,
812 "<InstanceID>0</InstanceID><Channel>Master</Channel>"
813 );
814 }
815
816 #[test]
817 fn test_get_loudness_parse_response_true() {
818 let xml_str =
819 r#"<GetLoudnessResponse><CurrentLoudness>1</CurrentLoudness></GetLoudnessResponse>"#;
820 let xml = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
821 let response = GetLoudnessOperation::parse_response(&xml).unwrap();
822 assert!(response.current_loudness);
823 }
824
825 #[test]
826 fn test_get_loudness_parse_response_false() {
827 let xml_str =
828 r#"<GetLoudnessResponse><CurrentLoudness>0</CurrentLoudness></GetLoudnessResponse>"#;
829 let xml = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
830 let response = GetLoudnessOperation::parse_response(&xml).unwrap();
831 assert!(!response.current_loudness);
832 }
833
834 #[test]
835 fn test_get_loudness_rejects_invalid_channel() {
836 let request = GetLoudnessOperationRequest {
837 instance_id: 0,
838 channel: "Invalid".to_string(),
839 };
840 assert!(request.validate_basic().is_err());
841 }
842
843 #[test]
844 fn test_set_loudness_builder() {
845 let op = set_loudness_operation("Master".to_string(), true)
846 .build()
847 .unwrap();
848 assert!(op.request().desired_loudness);
849 assert_eq!(op.request().channel, "Master");
850 }
851
852 #[test]
853 fn test_set_loudness_payload_true() {
854 let request = SetLoudnessOperationRequest {
855 instance_id: 0,
856 channel: "Master".to_string(),
857 desired_loudness: true,
858 };
859 let payload = SetLoudnessOperation::build_payload(&request).unwrap();
860 assert!(payload.contains("<DesiredLoudness>1</DesiredLoudness>"));
861 }
862
863 #[test]
864 fn test_set_loudness_payload_false() {
865 let request = SetLoudnessOperationRequest {
866 instance_id: 0,
867 channel: "Master".to_string(),
868 desired_loudness: false,
869 };
870 let payload = SetLoudnessOperation::build_payload(&request).unwrap();
871 assert!(payload.contains("<DesiredLoudness>0</DesiredLoudness>"));
872 }
873
874 #[test]
875 fn test_set_loudness_rejects_invalid_channel() {
876 let request = SetLoudnessOperationRequest {
877 instance_id: 0,
878 channel: "Invalid".to_string(),
879 desired_loudness: true,
880 };
881 assert!(request.validate_basic().is_err());
882 }
883}