1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::net::IpAddr;
9
10use crate::events::{xml_utils, EnrichedEvent, EventParser, EventSource};
11use crate::{ApiError, Result, Service};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(rename = "propertyset")]
16pub struct RenderingControlEvent {
17 #[serde(rename = "property")]
18 property: RenderingControlProperty,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22struct RenderingControlProperty {
23 #[serde(
24 rename = "LastChange",
25 deserialize_with = "xml_utils::deserialize_nested"
26 )]
27 last_change: RenderingControlEventData,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(rename = "Event")]
32pub struct RenderingControlEventData {
33 #[serde(rename = "InstanceID")]
34 instance: RenderingControlInstance,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38struct RenderingControlInstance {
39 #[serde(rename = "Volume", default)]
40 pub volumes: Vec<ChannelValueAttribute>,
41
42 #[serde(rename = "Mute", default)]
43 pub mutes: Vec<ChannelValueAttribute>,
44
45 #[serde(rename = "Bass", default)]
46 pub bass: Option<xml_utils::ValueAttribute>,
47
48 #[serde(rename = "Treble", default)]
49 pub treble: Option<xml_utils::ValueAttribute>,
50
51 #[serde(rename = "Loudness", default)]
52 pub loudness: Option<xml_utils::ValueAttribute>,
53
54 #[serde(rename = "Balance", default)]
55 pub balance: Option<xml_utils::ValueAttribute>,
56}
57
58#[derive(Debug, Clone, Deserialize, Serialize)]
60pub struct ChannelValueAttribute {
61 #[serde(rename = "@val", default)]
62 pub val: String,
63
64 #[serde(rename = "@channel", default)]
65 pub channel: String,
66}
67
68impl RenderingControlEvent {
69 pub fn master_volume(&self) -> Option<String> {
71 self.get_volume_for_channel("Master")
72 }
73
74 pub fn lf_volume(&self) -> Option<String> {
76 self.get_volume_for_channel("LF")
77 }
78
79 pub fn rf_volume(&self) -> Option<String> {
81 self.get_volume_for_channel("RF")
82 }
83
84 pub fn master_mute(&self) -> Option<String> {
86 self.get_mute_for_channel("Master")
87 }
88
89 pub fn lf_mute(&self) -> Option<String> {
91 self.get_mute_for_channel("LF")
92 }
93
94 pub fn rf_mute(&self) -> Option<String> {
96 self.get_mute_for_channel("RF")
97 }
98
99 pub fn bass(&self) -> Option<String> {
101 self.property
102 .last_change
103 .instance
104 .bass
105 .as_ref()
106 .map(|v| v.val.clone())
107 }
108
109 pub fn treble(&self) -> Option<String> {
111 self.property
112 .last_change
113 .instance
114 .treble
115 .as_ref()
116 .map(|v| v.val.clone())
117 }
118
119 pub fn loudness(&self) -> Option<String> {
121 self.property
122 .last_change
123 .instance
124 .loudness
125 .as_ref()
126 .map(|v| v.val.clone())
127 }
128
129 pub fn balance(&self) -> Option<String> {
131 self.property
132 .last_change
133 .instance
134 .balance
135 .as_ref()
136 .map(|v| v.val.clone())
137 }
138
139 pub fn other_channels(&self) -> HashMap<String, String> {
141 let mut channels = HashMap::new();
142
143 for volume in &self.property.last_change.instance.volumes {
145 if !["Master", "LF", "RF"].contains(&volume.channel.as_str()) {
146 channels.insert(format!("{}Volume", volume.channel), volume.val.clone());
147 }
148 }
149
150 for mute in &self.property.last_change.instance.mutes {
152 if !["Master", "LF", "RF"].contains(&mute.channel.as_str()) {
153 channels.insert(format!("{}Mute", mute.channel), mute.val.clone());
154 }
155 }
156
157 channels
158 }
159
160 fn get_volume_for_channel(&self, channel: &str) -> Option<String> {
162 self.property
163 .last_change
164 .instance
165 .volumes
166 .iter()
167 .find(|v| v.channel == channel)
168 .map(|v| v.val.clone())
169 }
170
171 fn get_mute_for_channel(&self, channel: &str) -> Option<String> {
173 self.property
174 .last_change
175 .instance
176 .mutes
177 .iter()
178 .find(|m| m.channel == channel)
179 .map(|m| m.val.clone())
180 }
181
182 pub fn into_state(&self) -> super::state::RenderingControlState {
184 super::state::RenderingControlState {
185 master_volume: self.master_volume(),
186 master_mute: self.master_mute(),
187 lf_volume: self.lf_volume(),
188 rf_volume: self.rf_volume(),
189 lf_mute: self.lf_mute(),
190 rf_mute: self.rf_mute(),
191 bass: self.bass(),
192 treble: self.treble(),
193 loudness: self.loudness(),
194 balance: self.balance(),
195 other_channels: self.other_channels(),
196 }
197 }
198
199 pub fn from_xml(xml: &str) -> Result<Self> {
201 let clean_xml = xml_utils::strip_namespaces(xml);
202 quick_xml::de::from_str(&clean_xml)
203 .map_err(|e| ApiError::ParseError(format!("Failed to parse RenderingControl XML: {e}")))
204 }
205}
206
207pub struct RenderingControlEventParser;
209
210impl EventParser for RenderingControlEventParser {
211 type EventData = RenderingControlEvent;
212
213 fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData> {
214 RenderingControlEvent::from_xml(xml)
215 }
216
217 fn service_type(&self) -> Service {
218 Service::RenderingControl
219 }
220}
221
222pub fn create_enriched_event(
224 speaker_ip: IpAddr,
225 event_source: EventSource,
226 event_data: RenderingControlEvent,
227) -> EnrichedEvent<RenderingControlEvent> {
228 EnrichedEvent::new(
229 speaker_ip,
230 Service::RenderingControl,
231 event_source,
232 event_data,
233 )
234}
235
236pub fn create_enriched_event_with_registration_id(
238 registration_id: u64,
239 speaker_ip: IpAddr,
240 event_source: EventSource,
241 event_data: RenderingControlEvent,
242) -> EnrichedEvent<RenderingControlEvent> {
243 EnrichedEvent::with_registration_id(
244 registration_id,
245 speaker_ip,
246 Service::RenderingControl,
247 event_source,
248 event_data,
249 )
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_rendering_control_parser_service_type() {
258 let parser = RenderingControlEventParser;
259 assert_eq!(parser.service_type(), Service::RenderingControl);
260 }
261
262 #[test]
263 fn test_rendering_control_event_creation() {
264 let event = RenderingControlEvent {
265 property: RenderingControlProperty {
266 last_change: RenderingControlEventData {
267 instance: RenderingControlInstance {
268 volumes: vec![ChannelValueAttribute {
269 val: "75".to_string(),
270 channel: "Master".to_string(),
271 }],
272 mutes: vec![ChannelValueAttribute {
273 val: "false".to_string(),
274 channel: "Master".to_string(),
275 }],
276 bass: Some(xml_utils::ValueAttribute {
277 val: "0".to_string(),
278 }),
279 treble: Some(xml_utils::ValueAttribute {
280 val: "0".to_string(),
281 }),
282 loudness: Some(xml_utils::ValueAttribute {
283 val: "true".to_string(),
284 }),
285 balance: Some(xml_utils::ValueAttribute {
286 val: "0".to_string(),
287 }),
288 },
289 },
290 },
291 };
292
293 assert_eq!(event.master_volume(), Some("75".to_string()));
294 assert_eq!(event.master_mute(), Some("false".to_string()));
295 assert!(event.other_channels().is_empty());
296 }
297
298 #[test]
299 fn test_basic_xml_parsing() {
300 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
301 <e:property>
302 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
303 <InstanceID val="0">
304 <Volume channel="Master" val="75"/>
305 <Mute channel="Master" val="0"/>
306 <Bass val="2"/>
307 <Treble val="-1"/>
308 </InstanceID>
309 </Event></LastChange>
310 </e:property>
311 </e:propertyset>"#;
312
313 let event = RenderingControlEvent::from_xml(xml).unwrap();
314 assert_eq!(event.master_volume(), Some("75".to_string()));
315 assert_eq!(event.master_mute(), Some("0".to_string()));
316 assert_eq!(event.bass(), Some("2".to_string()));
317 assert_eq!(event.treble(), Some("-1".to_string()));
318 }
319
320 #[test]
321 fn test_channel_specific_volume() {
322 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
323 <e:property>
324 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
325 <InstanceID val="0">
326 <Volume channel="Master" val="50"/>
327 <Volume channel="LF" val="80"/>
328 <Volume channel="RF" val="85"/>
329 <Mute channel="LF" val="1"/>
330 </InstanceID>
331 </Event></LastChange>
332 </e:property>
333 </e:propertyset>"#;
334
335 let event = RenderingControlEvent::from_xml(xml).unwrap();
336 assert_eq!(event.master_volume(), Some("50".to_string()));
337 assert_eq!(event.lf_volume(), Some("80".to_string()));
338 assert_eq!(event.rf_volume(), Some("85".to_string()));
339 assert_eq!(event.lf_mute(), Some("1".to_string()));
340 }
341
342 #[test]
343 fn test_enriched_event_creation() {
344 let ip: IpAddr = "192.168.1.100".parse().unwrap();
345 let source = EventSource::UPnPNotification {
346 subscription_id: "uuid:123".to_string(),
347 };
348 let event_data = RenderingControlEvent {
349 property: RenderingControlProperty {
350 last_change: RenderingControlEventData {
351 instance: RenderingControlInstance {
352 volumes: vec![ChannelValueAttribute {
353 val: "50".to_string(),
354 channel: "Master".to_string(),
355 }],
356 mutes: vec![ChannelValueAttribute {
357 val: "0".to_string(),
358 channel: "Master".to_string(),
359 }],
360 bass: None,
361 treble: None,
362 loudness: None,
363 balance: None,
364 },
365 },
366 },
367 };
368
369 let enriched = create_enriched_event(ip, source, event_data);
370
371 assert_eq!(enriched.speaker_ip, ip);
372 assert_eq!(enriched.service, Service::RenderingControl);
373 assert!(enriched.registration_id.is_none());
374 }
375
376 #[test]
377 fn test_enriched_event_with_registration_id() {
378 let ip: IpAddr = "192.168.1.100".parse().unwrap();
379 let source = EventSource::UPnPNotification {
380 subscription_id: "uuid:123".to_string(),
381 };
382 let event_data = RenderingControlEvent {
383 property: RenderingControlProperty {
384 last_change: RenderingControlEventData {
385 instance: RenderingControlInstance {
386 volumes: vec![ChannelValueAttribute {
387 val: "50".to_string(),
388 channel: "Master".to_string(),
389 }],
390 mutes: vec![ChannelValueAttribute {
391 val: "0".to_string(),
392 channel: "Master".to_string(),
393 }],
394 bass: None,
395 treble: None,
396 loudness: None,
397 balance: None,
398 },
399 },
400 },
401 };
402
403 let enriched = create_enriched_event_with_registration_id(42, ip, source, event_data);
404
405 assert_eq!(enriched.registration_id, Some(42));
406 }
407
408 #[test]
409 fn test_into_state_maps_all_fields() {
410 let event = RenderingControlEvent {
411 property: RenderingControlProperty {
412 last_change: RenderingControlEventData {
413 instance: RenderingControlInstance {
414 volumes: vec![
415 ChannelValueAttribute {
416 val: "50".to_string(),
417 channel: "Master".to_string(),
418 },
419 ChannelValueAttribute {
420 val: "45".to_string(),
421 channel: "LF".to_string(),
422 },
423 ChannelValueAttribute {
424 val: "55".to_string(),
425 channel: "RF".to_string(),
426 },
427 ],
428 mutes: vec![ChannelValueAttribute {
429 val: "0".to_string(),
430 channel: "Master".to_string(),
431 }],
432 bass: Some(xml_utils::ValueAttribute {
433 val: "5".to_string(),
434 }),
435 treble: Some(xml_utils::ValueAttribute {
436 val: "-3".to_string(),
437 }),
438 loudness: Some(xml_utils::ValueAttribute {
439 val: "1".to_string(),
440 }),
441 balance: None,
442 },
443 },
444 },
445 };
446
447 let state = event.into_state();
448
449 assert_eq!(state.master_volume, Some("50".to_string()));
450 assert_eq!(state.master_mute, Some("0".to_string()));
451 assert_eq!(state.lf_volume, Some("45".to_string()));
452 assert_eq!(state.rf_volume, Some("55".to_string()));
453 assert_eq!(state.bass, Some("5".to_string()));
454 assert_eq!(state.treble, Some("-3".to_string()));
455 assert_eq!(state.loudness, Some("1".to_string()));
456 }
457}