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]
540 fn test_service_level_subscription_helpers() {
541 let client = crate::SonosClient::new();
543
544 let _subscribe_fn = || subscribe(&client, "192.168.1.100", "http://callback.url");
546
547 let _subscribe_timeout_fn =
549 || subscribe_with_timeout(&client, "192.168.1.100", "http://callback.url", 3600);
550 }
551
552 #[test]
553 fn test_subscription_uses_correct_service() {
554 let _client = crate::SonosClient::new();
556
557 assert_eq!(SERVICE, crate::Service::RenderingControl);
559
560 }
564
565 #[test]
570 fn test_get_mute_builder() {
571 let op = get_mute_operation("Master".to_string()).build().unwrap();
572 assert_eq!(op.request().channel, "Master");
573 assert_eq!(op.request().instance_id, 0);
574 }
575
576 #[test]
577 fn test_get_mute_payload() {
578 let request = GetMuteOperationRequest {
579 instance_id: 0,
580 channel: "Master".to_string(),
581 };
582 let payload = GetMuteOperation::build_payload(&request).unwrap();
583 assert_eq!(
584 payload,
585 "<InstanceID>0</InstanceID><Channel>Master</Channel>"
586 );
587 }
588
589 #[test]
590 fn test_get_mute_parse_response_true() {
591 let xml_str = r#"<GetMuteResponse><CurrentMute>1</CurrentMute></GetMuteResponse>"#;
592 let xml = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
593 let response = GetMuteOperation::parse_response(&xml).unwrap();
594 assert!(response.current_mute);
595 }
596
597 #[test]
598 fn test_get_mute_parse_response_false() {
599 let xml_str = r#"<GetMuteResponse><CurrentMute>0</CurrentMute></GetMuteResponse>"#;
600 let xml = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
601 let response = GetMuteOperation::parse_response(&xml).unwrap();
602 assert!(!response.current_mute);
603 }
604
605 #[test]
606 fn test_get_mute_rejects_invalid_channel() {
607 let request = GetMuteOperationRequest {
608 instance_id: 0,
609 channel: "Invalid".to_string(),
610 };
611 assert!(request.validate_basic().is_err());
612 }
613
614 #[test]
615 fn test_set_mute_builder() {
616 let op = set_mute_operation("Master".to_string(), true)
617 .build()
618 .unwrap();
619 assert!(op.request().desired_mute);
620 assert_eq!(op.request().channel, "Master");
621 }
622
623 #[test]
624 fn test_set_mute_payload_true() {
625 let request = SetMuteOperationRequest {
626 instance_id: 0,
627 channel: "Master".to_string(),
628 desired_mute: true,
629 };
630 let payload = SetMuteOperation::build_payload(&request).unwrap();
631 assert!(payload.contains("<DesiredMute>1</DesiredMute>"));
632 }
633
634 #[test]
635 fn test_set_mute_payload_false() {
636 let request = SetMuteOperationRequest {
637 instance_id: 0,
638 channel: "Master".to_string(),
639 desired_mute: false,
640 };
641 let payload = SetMuteOperation::build_payload(&request).unwrap();
642 assert!(payload.contains("<DesiredMute>0</DesiredMute>"));
643 }
644
645 #[test]
646 fn test_set_mute_rejects_invalid_channel() {
647 let request = SetMuteOperationRequest {
648 instance_id: 0,
649 channel: "Invalid".to_string(),
650 desired_mute: true,
651 };
652 assert!(request.validate_basic().is_err());
653 }
654
655 #[test]
660 fn test_get_bass_builder() {
661 let op = get_bass_operation().build().unwrap();
662 assert_eq!(op.request().instance_id, 0);
663 }
664
665 #[test]
666 fn test_get_bass_payload() {
667 let request = GetBassOperationRequest { instance_id: 0 };
668 let payload = GetBassOperation::build_payload(&request).unwrap();
669 assert_eq!(payload, "<InstanceID>0</InstanceID>");
670 }
671
672 #[test]
673 fn test_set_bass_builder() {
674 let op = set_bass_operation(5).build().unwrap();
675 assert_eq!(op.request().desired_bass, 5);
676 }
677
678 #[test]
679 fn test_set_bass_payload() {
680 let request = SetBassOperationRequest {
681 instance_id: 0,
682 desired_bass: -5,
683 };
684 let payload = SetBassOperation::build_payload(&request).unwrap();
685 assert!(payload.contains("<DesiredBass>-5</DesiredBass>"));
686 }
687
688 #[test]
689 fn test_set_bass_validation() {
690 let request = SetBassOperationRequest {
692 instance_id: 0,
693 desired_bass: -10,
694 };
695 assert!(request.validate_basic().is_ok());
696 let request = SetBassOperationRequest {
697 instance_id: 0,
698 desired_bass: 10,
699 };
700 assert!(request.validate_basic().is_ok());
701
702 let request = SetBassOperationRequest {
704 instance_id: 0,
705 desired_bass: -11,
706 };
707 assert!(request.validate_basic().is_err());
708 let request = SetBassOperationRequest {
709 instance_id: 0,
710 desired_bass: 11,
711 };
712 assert!(request.validate_basic().is_err());
713 }
714
715 #[test]
720 fn test_get_treble_builder() {
721 let op = get_treble_operation().build().unwrap();
722 assert_eq!(op.request().instance_id, 0);
723 }
724
725 #[test]
726 fn test_get_treble_payload() {
727 let request = GetTrebleOperationRequest { instance_id: 0 };
728 let payload = GetTrebleOperation::build_payload(&request).unwrap();
729 assert_eq!(payload, "<InstanceID>0</InstanceID>");
730 }
731
732 #[test]
733 fn test_set_treble_builder() {
734 let op = set_treble_operation(-3).build().unwrap();
735 assert_eq!(op.request().desired_treble, -3);
736 }
737
738 #[test]
739 fn test_set_treble_payload() {
740 let request = SetTrebleOperationRequest {
741 instance_id: 0,
742 desired_treble: 7,
743 };
744 let payload = SetTrebleOperation::build_payload(&request).unwrap();
745 assert!(payload.contains("<DesiredTreble>7</DesiredTreble>"));
746 }
747
748 #[test]
749 fn test_set_treble_validation() {
750 let request = SetTrebleOperationRequest {
751 instance_id: 0,
752 desired_treble: -10,
753 };
754 assert!(request.validate_basic().is_ok());
755 let request = SetTrebleOperationRequest {
756 instance_id: 0,
757 desired_treble: 10,
758 };
759 assert!(request.validate_basic().is_ok());
760
761 let request = SetTrebleOperationRequest {
762 instance_id: 0,
763 desired_treble: -11,
764 };
765 assert!(request.validate_basic().is_err());
766 let request = SetTrebleOperationRequest {
767 instance_id: 0,
768 desired_treble: 11,
769 };
770 assert!(request.validate_basic().is_err());
771 }
772
773 #[test]
778 fn test_get_loudness_builder() {
779 let op = get_loudness_operation("Master".to_string())
780 .build()
781 .unwrap();
782 assert_eq!(op.request().channel, "Master");
783 assert_eq!(op.request().instance_id, 0);
784 }
785
786 #[test]
787 fn test_get_loudness_payload() {
788 let request = GetLoudnessOperationRequest {
789 instance_id: 0,
790 channel: "Master".to_string(),
791 };
792 let payload = GetLoudnessOperation::build_payload(&request).unwrap();
793 assert_eq!(
794 payload,
795 "<InstanceID>0</InstanceID><Channel>Master</Channel>"
796 );
797 }
798
799 #[test]
800 fn test_get_loudness_parse_response_true() {
801 let xml_str =
802 r#"<GetLoudnessResponse><CurrentLoudness>1</CurrentLoudness></GetLoudnessResponse>"#;
803 let xml = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
804 let response = GetLoudnessOperation::parse_response(&xml).unwrap();
805 assert!(response.current_loudness);
806 }
807
808 #[test]
809 fn test_get_loudness_parse_response_false() {
810 let xml_str =
811 r#"<GetLoudnessResponse><CurrentLoudness>0</CurrentLoudness></GetLoudnessResponse>"#;
812 let xml = xmltree::Element::parse(xml_str.as_bytes()).unwrap();
813 let response = GetLoudnessOperation::parse_response(&xml).unwrap();
814 assert!(!response.current_loudness);
815 }
816
817 #[test]
818 fn test_get_loudness_rejects_invalid_channel() {
819 let request = GetLoudnessOperationRequest {
820 instance_id: 0,
821 channel: "Invalid".to_string(),
822 };
823 assert!(request.validate_basic().is_err());
824 }
825
826 #[test]
827 fn test_set_loudness_builder() {
828 let op = set_loudness_operation("Master".to_string(), true)
829 .build()
830 .unwrap();
831 assert!(op.request().desired_loudness);
832 assert_eq!(op.request().channel, "Master");
833 }
834
835 #[test]
836 fn test_set_loudness_payload_true() {
837 let request = SetLoudnessOperationRequest {
838 instance_id: 0,
839 channel: "Master".to_string(),
840 desired_loudness: true,
841 };
842 let payload = SetLoudnessOperation::build_payload(&request).unwrap();
843 assert!(payload.contains("<DesiredLoudness>1</DesiredLoudness>"));
844 }
845
846 #[test]
847 fn test_set_loudness_payload_false() {
848 let request = SetLoudnessOperationRequest {
849 instance_id: 0,
850 channel: "Master".to_string(),
851 desired_loudness: false,
852 };
853 let payload = SetLoudnessOperation::build_payload(&request).unwrap();
854 assert!(payload.contains("<DesiredLoudness>0</DesiredLoudness>"));
855 }
856
857 #[test]
858 fn test_set_loudness_rejects_invalid_channel() {
859 let request = SetLoudnessOperationRequest {
860 instance_id: 0,
861 channel: "Invalid".to_string(),
862 desired_loudness: true,
863 };
864 assert!(request.validate_basic().is_err());
865 }
866}