Skip to main content

sonos_api/services/group_rendering_control/
events.rs

1//! GroupRenderingControl service event types and parsing
2//!
3//! Provides direct serde-based XML parsing with no business logic,
4//! replicating exactly what Sonos produces for sonos-stream consumption.
5//!
6//! GroupRenderingControl uses a direct property structure (not LastChange-wrapped):
7//! ```xml
8//! <e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
9//!   <e:property><GroupVolume>14</GroupVolume></e:property>
10//!   <e:property><GroupMute>0</GroupMute></e:property>
11//!   <e:property><GroupVolumeChangeable>1</GroupVolumeChangeable></e:property>
12//! </e:propertyset>
13//! ```
14
15use serde::{Deserialize, Serialize};
16
17use crate::{ApiError, Result};
18
19/// GroupRenderingControl event - direct serde mapping from UPnP event XML
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(rename = "propertyset")]
22pub struct GroupRenderingControlEvent {
23    #[serde(rename = "property", default)]
24    properties: Vec<GroupRenderingControlProperty>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28struct GroupRenderingControlProperty {
29    #[serde(rename = "GroupVolume", default)]
30    group_volume: Option<String>,
31
32    #[serde(rename = "GroupMute", default)]
33    group_mute: Option<String>,
34
35    #[serde(rename = "GroupVolumeChangeable", default)]
36    group_volume_changeable: Option<String>,
37}
38
39impl GroupRenderingControlEvent {
40    /// Get the group volume level (0-100)
41    pub fn group_volume(&self) -> Option<u16> {
42        self.properties
43            .iter()
44            .find_map(|p| p.group_volume.as_ref())
45            .and_then(|s| s.parse::<u16>().ok())
46    }
47
48    /// Get the group mute state
49    pub fn group_mute(&self) -> Option<bool> {
50        self.properties
51            .iter()
52            .find_map(|p| p.group_mute.as_ref())
53            .map(|s| s == "1" || s.to_lowercase() == "true")
54    }
55
56    /// Get whether the group volume is changeable
57    pub fn group_volume_changeable(&self) -> Option<bool> {
58        self.properties
59            .iter()
60            .find_map(|p| p.group_volume_changeable.as_ref())
61            .map(|s| s == "1" || s.to_lowercase() == "true")
62    }
63
64    /// Convert parsed UPnP event to canonical state representation.
65    pub fn into_state(&self) -> super::state::GroupRenderingControlState {
66        super::state::GroupRenderingControlState {
67            group_volume: self.group_volume(),
68            group_mute: self.group_mute(),
69            group_volume_changeable: self.group_volume_changeable(),
70        }
71    }
72
73    /// Parse from UPnP event XML using serde
74    pub fn from_xml(xml: &str) -> Result<Self> {
75        quick_xml::de::from_str(xml).map_err(|e| {
76            ApiError::ParseError(format!("Failed to parse GroupRenderingControl XML: {e}"))
77        })
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_parse_real_event_xml() {
87        // Captured from a real Sonos Amp (Living Room)
88        let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0"><e:property><GroupVolume>14</GroupVolume></e:property><e:property><GroupMute>0</GroupMute></e:property><e:property><GroupVolumeChangeable>1</GroupVolumeChangeable></e:property></e:propertyset>"#;
89
90        let event = GroupRenderingControlEvent::from_xml(xml).unwrap();
91        assert_eq!(event.group_volume(), Some(14));
92        assert_eq!(event.group_mute(), Some(false));
93        assert_eq!(event.group_volume_changeable(), Some(true));
94    }
95
96    #[test]
97    fn test_parse_formatted_xml() {
98        let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
99            <e:property>
100                <GroupVolume>75</GroupVolume>
101            </e:property>
102            <e:property>
103                <GroupMute>1</GroupMute>
104            </e:property>
105            <e:property>
106                <GroupVolumeChangeable>0</GroupVolumeChangeable>
107            </e:property>
108        </e:propertyset>"#;
109
110        let event = GroupRenderingControlEvent::from_xml(xml).unwrap();
111        assert_eq!(event.group_volume(), Some(75));
112        assert_eq!(event.group_mute(), Some(true));
113        assert_eq!(event.group_volume_changeable(), Some(false));
114    }
115
116    #[test]
117    fn test_partial_properties() {
118        let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
119            <e:property>
120                <GroupVolume>50</GroupVolume>
121            </e:property>
122        </e:propertyset>"#;
123
124        let event = GroupRenderingControlEvent::from_xml(xml).unwrap();
125        assert_eq!(event.group_volume(), Some(50));
126        assert_eq!(event.group_mute(), None);
127        assert_eq!(event.group_volume_changeable(), None);
128    }
129
130    #[test]
131    fn test_volume_boundary_values() {
132        let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
133            <e:property><GroupVolume>0</GroupVolume></e:property>
134        </e:propertyset>"#;
135        let event = GroupRenderingControlEvent::from_xml(xml).unwrap();
136        assert_eq!(event.group_volume(), Some(0));
137
138        let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
139            <e:property><GroupVolume>100</GroupVolume></e:property>
140        </e:propertyset>"#;
141        let event = GroupRenderingControlEvent::from_xml(xml).unwrap();
142        assert_eq!(event.group_volume(), Some(100));
143    }
144
145    #[test]
146    fn test_boolean_parsing() {
147        let event = GroupRenderingControlEvent {
148            properties: vec![GroupRenderingControlProperty {
149                group_volume: None,
150                group_mute: Some("1".to_string()),
151                group_volume_changeable: Some("true".to_string()),
152            }],
153        };
154        assert_eq!(event.group_mute(), Some(true));
155        assert_eq!(event.group_volume_changeable(), Some(true));
156
157        let event = GroupRenderingControlEvent {
158            properties: vec![GroupRenderingControlProperty {
159                group_volume: None,
160                group_mute: Some("0".to_string()),
161                group_volume_changeable: Some("false".to_string()),
162            }],
163        };
164        assert_eq!(event.group_mute(), Some(false));
165        assert_eq!(event.group_volume_changeable(), Some(false));
166    }
167
168    #[test]
169    fn test_into_state_maps_all_fields() {
170        let event = GroupRenderingControlEvent {
171            properties: vec![GroupRenderingControlProperty {
172                group_volume: Some("42".to_string()),
173                group_mute: Some("0".to_string()),
174                group_volume_changeable: Some("true".to_string()),
175            }],
176        };
177
178        let state = event.into_state();
179
180        assert_eq!(state.group_volume, Some(42));
181        assert_eq!(state.group_mute, Some(false));
182        assert_eq!(state.group_volume_changeable, Some(true));
183    }
184}