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)]
50 pub bass: Vec<ChannelValueAttribute>,
51
52 #[serde(rename = "Treble", default)]
53 pub treble: Vec<ChannelValueAttribute>,
54
55 #[serde(rename = "Loudness", default)]
56 pub loudness: Vec<ChannelValueAttribute>,
57
58 #[serde(rename = "Balance", default)]
59 pub balance: Vec<ChannelValueAttribute>,
60}
61
62#[derive(Debug, Clone, Deserialize, Serialize)]
64pub struct ChannelValueAttribute {
65 #[serde(rename = "@val", default)]
66 pub val: String,
67
68 #[serde(rename = "@channel", default)]
69 pub channel: String,
70}
71
72impl RenderingControlEvent {
73 pub fn master_volume(&self) -> Option<String> {
75 self.get_volume_for_channel("Master")
76 }
77
78 pub fn lf_volume(&self) -> Option<String> {
80 self.get_volume_for_channel("LF")
81 }
82
83 pub fn rf_volume(&self) -> Option<String> {
85 self.get_volume_for_channel("RF")
86 }
87
88 pub fn master_mute(&self) -> Option<String> {
90 self.get_mute_for_channel("Master")
91 }
92
93 pub fn lf_mute(&self) -> Option<String> {
95 self.get_mute_for_channel("LF")
96 }
97
98 pub fn rf_mute(&self) -> Option<String> {
100 self.get_mute_for_channel("RF")
101 }
102
103 pub fn bass(&self) -> Option<String> {
105 Self::master_value(&self.property.last_change.instance.bass)
106 }
107
108 pub fn treble(&self) -> Option<String> {
110 Self::master_value(&self.property.last_change.instance.treble)
111 }
112
113 pub fn loudness(&self) -> Option<String> {
115 Self::master_value(&self.property.last_change.instance.loudness)
116 }
117
118 pub fn balance(&self) -> Option<String> {
120 Self::master_value(&self.property.last_change.instance.balance)
121 }
122
123 pub fn other_channels(&self) -> HashMap<String, String> {
125 let mut channels = HashMap::new();
126
127 for volume in &self.property.last_change.instance.volumes {
129 if !["Master", "LF", "RF"].contains(&volume.channel.as_str()) {
130 channels.insert(format!("{}Volume", volume.channel), volume.val.clone());
131 }
132 }
133
134 for mute in &self.property.last_change.instance.mutes {
136 if !["Master", "LF", "RF"].contains(&mute.channel.as_str()) {
137 channels.insert(format!("{}Mute", mute.channel), mute.val.clone());
138 }
139 }
140
141 channels
142 }
143
144 fn get_volume_for_channel(&self, channel: &str) -> Option<String> {
146 self.property
147 .last_change
148 .instance
149 .volumes
150 .iter()
151 .find(|v| v.channel == channel)
152 .map(|v| v.val.clone())
153 }
154
155 fn master_value(values: &[ChannelValueAttribute]) -> Option<String> {
164 values
165 .iter()
166 .find(|v| v.channel == "Master" || v.channel.is_empty())
167 .map(|v| v.val.clone())
168 }
169
170 fn get_mute_for_channel(&self, channel: &str) -> Option<String> {
172 self.property
173 .last_change
174 .instance
175 .mutes
176 .iter()
177 .find(|m| m.channel == channel)
178 .map(|m| m.val.clone())
179 }
180
181 pub fn into_state(&self) -> super::state::RenderingControlState {
183 super::state::RenderingControlState {
184 master_volume: self.master_volume(),
185 master_mute: self.master_mute(),
186 lf_volume: self.lf_volume(),
187 rf_volume: self.rf_volume(),
188 lf_mute: self.lf_mute(),
189 rf_mute: self.rf_mute(),
190 bass: self.bass(),
191 treble: self.treble(),
192 loudness: self.loudness(),
193 balance: self.balance(),
194 other_channels: self.other_channels(),
195 }
196 }
197
198 pub fn from_xml(xml: &str) -> Result<Self> {
200 let clean_xml = xml_utils::strip_namespaces(xml);
201 quick_xml::de::from_str(&clean_xml)
202 .map_err(|e| ApiError::ParseError(format!("Failed to parse RenderingControl XML: {e}")))
203 }
204}
205
206pub struct RenderingControlEventParser;
208
209impl EventParser for RenderingControlEventParser {
210 type EventData = RenderingControlEvent;
211
212 fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData> {
213 RenderingControlEvent::from_xml(xml)
214 }
215
216 fn service_type(&self) -> Service {
217 Service::RenderingControl
218 }
219}
220
221pub fn create_enriched_event(
223 speaker_ip: IpAddr,
224 event_source: EventSource,
225 event_data: RenderingControlEvent,
226) -> EnrichedEvent<RenderingControlEvent> {
227 EnrichedEvent::new(
228 speaker_ip,
229 Service::RenderingControl,
230 event_source,
231 event_data,
232 )
233}
234
235pub fn create_enriched_event_with_registration_id(
237 registration_id: u64,
238 speaker_ip: IpAddr,
239 event_source: EventSource,
240 event_data: RenderingControlEvent,
241) -> EnrichedEvent<RenderingControlEvent> {
242 EnrichedEvent::with_registration_id(
243 registration_id,
244 speaker_ip,
245 Service::RenderingControl,
246 event_source,
247 event_data,
248 )
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 fn master(val: &str) -> ChannelValueAttribute {
257 ChannelValueAttribute {
258 val: val.to_string(),
259 channel: "Master".to_string(),
260 }
261 }
262
263 #[test]
264 fn test_rendering_control_parser_service_type() {
265 let parser = RenderingControlEventParser;
266 assert_eq!(parser.service_type(), Service::RenderingControl);
267 }
268
269 #[test]
270 fn test_rendering_control_event_creation() {
271 let event = RenderingControlEvent {
272 property: RenderingControlProperty {
273 last_change: RenderingControlEventData {
274 instance: RenderingControlInstance {
275 volumes: vec![ChannelValueAttribute {
276 val: "75".to_string(),
277 channel: "Master".to_string(),
278 }],
279 mutes: vec![ChannelValueAttribute {
280 val: "false".to_string(),
281 channel: "Master".to_string(),
282 }],
283 bass: vec![master("0")],
284 treble: vec![master("0")],
285 loudness: vec![master("true")],
286 balance: vec![master("0")],
287 },
288 },
289 },
290 };
291
292 assert_eq!(event.master_volume(), Some("75".to_string()));
293 assert_eq!(event.master_mute(), Some("false".to_string()));
294 assert!(event.other_channels().is_empty());
295 }
296
297 #[test]
298 fn test_basic_xml_parsing() {
299 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
300 <e:property>
301 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
302 <InstanceID val="0">
303 <Volume channel="Master" val="75"/>
304 <Mute channel="Master" val="0"/>
305 <Bass val="2"/>
306 <Treble val="-1"/>
307 </InstanceID>
308 </Event></LastChange>
309 </e:property>
310 </e:propertyset>"#;
311
312 let event = RenderingControlEvent::from_xml(xml).unwrap();
313 assert_eq!(event.master_volume(), Some("75".to_string()));
314 assert_eq!(event.master_mute(), Some("0".to_string()));
315 assert_eq!(event.bass(), Some("2".to_string()));
316 assert_eq!(event.treble(), Some("-1".to_string()));
317 }
318
319 #[test]
320 fn test_channel_specific_volume() {
321 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
322 <e:property>
323 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
324 <InstanceID val="0">
325 <Volume channel="Master" val="50"/>
326 <Volume channel="LF" val="80"/>
327 <Volume channel="RF" val="85"/>
328 <Mute channel="LF" val="1"/>
329 </InstanceID>
330 </Event></LastChange>
331 </e:property>
332 </e:propertyset>"#;
333
334 let event = RenderingControlEvent::from_xml(xml).unwrap();
335 assert_eq!(event.master_volume(), Some("50".to_string()));
336 assert_eq!(event.lf_volume(), Some("80".to_string()));
337 assert_eq!(event.rf_volume(), Some("85".to_string()));
338 assert_eq!(event.lf_mute(), Some("1".to_string()));
339 }
340
341 #[test]
342 fn test_enriched_event_creation() {
343 let ip: IpAddr = "192.168.1.100".parse().unwrap();
344 let source = EventSource::UPnPNotification {
345 subscription_id: "uuid:123".to_string(),
346 };
347 let event_data = RenderingControlEvent {
348 property: RenderingControlProperty {
349 last_change: RenderingControlEventData {
350 instance: RenderingControlInstance {
351 volumes: vec![ChannelValueAttribute {
352 val: "50".to_string(),
353 channel: "Master".to_string(),
354 }],
355 mutes: vec![ChannelValueAttribute {
356 val: "0".to_string(),
357 channel: "Master".to_string(),
358 }],
359 bass: vec![],
360 treble: vec![],
361 loudness: vec![],
362 balance: vec![],
363 },
364 },
365 },
366 };
367
368 let enriched = create_enriched_event(ip, source, event_data);
369
370 assert_eq!(enriched.speaker_ip, ip);
371 assert_eq!(enriched.service, Service::RenderingControl);
372 assert!(enriched.registration_id.is_none());
373 }
374
375 #[test]
376 fn test_enriched_event_with_registration_id() {
377 let ip: IpAddr = "192.168.1.100".parse().unwrap();
378 let source = EventSource::UPnPNotification {
379 subscription_id: "uuid:123".to_string(),
380 };
381 let event_data = RenderingControlEvent {
382 property: RenderingControlProperty {
383 last_change: RenderingControlEventData {
384 instance: RenderingControlInstance {
385 volumes: vec![ChannelValueAttribute {
386 val: "50".to_string(),
387 channel: "Master".to_string(),
388 }],
389 mutes: vec![ChannelValueAttribute {
390 val: "0".to_string(),
391 channel: "Master".to_string(),
392 }],
393 bass: vec![],
394 treble: vec![],
395 loudness: vec![],
396 balance: vec![],
397 },
398 },
399 },
400 };
401
402 let enriched = create_enriched_event_with_registration_id(42, ip, source, event_data);
403
404 assert_eq!(enriched.registration_id, Some(42));
405 }
406
407 #[test]
408 fn test_lf_only_loudness_is_not_reported_as_master() {
409 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
410 <e:property>
411 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
412 <InstanceID val="0">
413 <Loudness channel="LF" val="1"/>
414 </InstanceID>
415 </Event></LastChange>
416 </e:property>
417 </e:propertyset>"#;
418
419 let event = RenderingControlEvent::from_xml(xml).unwrap();
420 assert_eq!(event.loudness(), None);
421 }
422
423 #[test]
424 fn test_duplicate_loudness_channels_do_not_drop_event() {
425 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
426 <e:property>
427 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
428 <InstanceID val="0">
429 <Volume channel="Master" val="42"/>
430 <Mute channel="Master" val="0"/>
431 <Loudness channel="Master" val="1"/>
432 <Loudness channel="LF" val="0"/>
433 </InstanceID>
434 </Event></LastChange>
435 </e:property>
436 </e:propertyset>"#;
437
438 let parsed = RenderingControlEvent::from_xml(xml);
439 assert!(
440 parsed.is_ok(),
441 "per-channel Loudness must not drop the event"
442 );
443
444 let event = parsed.unwrap();
445 assert_eq!(event.master_volume(), Some("42".to_string()));
447 assert_eq!(event.master_mute(), Some("0".to_string()));
448 assert_eq!(event.loudness(), Some("1".to_string()));
449 }
450
451 #[test]
452 fn test_scalar_bass_still_parses() {
453 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
454 <e:property>
455 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
456 <InstanceID val="0">
457 <Bass val="2"/>
458 </InstanceID>
459 </Event></LastChange>
460 </e:property>
461 </e:propertyset>"#;
462
463 let event = RenderingControlEvent::from_xml(xml).unwrap();
464 assert_eq!(event.bass(), Some("2".to_string()));
465 }
466
467 #[test]
468 fn test_into_state_maps_all_fields() {
469 let event = RenderingControlEvent {
470 property: RenderingControlProperty {
471 last_change: RenderingControlEventData {
472 instance: RenderingControlInstance {
473 volumes: vec![
474 ChannelValueAttribute {
475 val: "50".to_string(),
476 channel: "Master".to_string(),
477 },
478 ChannelValueAttribute {
479 val: "45".to_string(),
480 channel: "LF".to_string(),
481 },
482 ChannelValueAttribute {
483 val: "55".to_string(),
484 channel: "RF".to_string(),
485 },
486 ],
487 mutes: vec![ChannelValueAttribute {
488 val: "0".to_string(),
489 channel: "Master".to_string(),
490 }],
491 bass: vec![master("5")],
492 treble: vec![master("-3")],
493 loudness: vec![master("1")],
494 balance: vec![],
495 },
496 },
497 },
498 };
499
500 let state = event.into_state();
501
502 assert_eq!(state.master_volume, Some("50".to_string()));
503 assert_eq!(state.master_mute, Some("0".to_string()));
504 assert_eq!(state.lf_volume, Some("45".to_string()));
505 assert_eq!(state.rf_volume, Some("55".to_string()));
506 assert_eq!(state.bass, Some("5".to_string()));
507 assert_eq!(state.treble, Some("-3".to_string()));
508 assert_eq!(state.loudness, Some("1".to_string()));
509 }
510}