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