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 quick_xml::de::from_str(xml)
201 .map_err(|e| ApiError::ParseError(format!("Failed to parse RenderingControl XML: {e}")))
202 }
203}
204
205pub struct RenderingControlEventParser;
207
208impl EventParser for RenderingControlEventParser {
209 type EventData = RenderingControlEvent;
210
211 fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData> {
212 RenderingControlEvent::from_xml(xml)
213 }
214
215 fn service_type(&self) -> Service {
216 Service::RenderingControl
217 }
218}
219
220pub fn create_enriched_event(
222 speaker_ip: IpAddr,
223 event_source: EventSource,
224 event_data: RenderingControlEvent,
225) -> EnrichedEvent<RenderingControlEvent> {
226 EnrichedEvent::new(
227 speaker_ip,
228 Service::RenderingControl,
229 event_source,
230 event_data,
231 )
232}
233
234pub fn create_enriched_event_with_registration_id(
236 registration_id: u64,
237 speaker_ip: IpAddr,
238 event_source: EventSource,
239 event_data: RenderingControlEvent,
240) -> EnrichedEvent<RenderingControlEvent> {
241 EnrichedEvent::with_registration_id(
242 registration_id,
243 speaker_ip,
244 Service::RenderingControl,
245 event_source,
246 event_data,
247 )
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 fn master(val: &str) -> ChannelValueAttribute {
256 ChannelValueAttribute {
257 val: val.to_string(),
258 channel: "Master".to_string(),
259 }
260 }
261
262 #[test]
263 fn test_rendering_control_parser_service_type() {
264 let parser = RenderingControlEventParser;
265 assert_eq!(parser.service_type(), Service::RenderingControl);
266 }
267
268 #[test]
269 fn test_rendering_control_event_creation() {
270 let event = RenderingControlEvent {
271 property: RenderingControlProperty {
272 last_change: RenderingControlEventData {
273 instance: RenderingControlInstance {
274 volumes: vec![ChannelValueAttribute {
275 val: "75".to_string(),
276 channel: "Master".to_string(),
277 }],
278 mutes: vec![ChannelValueAttribute {
279 val: "false".to_string(),
280 channel: "Master".to_string(),
281 }],
282 bass: vec![master("0")],
283 treble: vec![master("0")],
284 loudness: vec![master("true")],
285 balance: vec![master("0")],
286 },
287 },
288 },
289 };
290
291 assert_eq!(event.master_volume(), Some("75".to_string()));
292 assert_eq!(event.master_mute(), Some("false".to_string()));
293 assert!(event.other_channels().is_empty());
294 }
295
296 #[test]
297 fn test_basic_xml_parsing() {
298 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
299 <e:property>
300 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
301 <InstanceID val="0">
302 <Volume channel="Master" val="75"/>
303 <Mute channel="Master" val="0"/>
304 <Bass val="2"/>
305 <Treble val="-1"/>
306 </InstanceID>
307 </Event></LastChange>
308 </e:property>
309 </e:propertyset>"#;
310
311 let event = RenderingControlEvent::from_xml(xml).unwrap();
312 assert_eq!(event.master_volume(), Some("75".to_string()));
313 assert_eq!(event.master_mute(), Some("0".to_string()));
314 assert_eq!(event.bass(), Some("2".to_string()));
315 assert_eq!(event.treble(), Some("-1".to_string()));
316 }
317
318 #[test]
319 fn test_channel_specific_volume() {
320 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
321 <e:property>
322 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
323 <InstanceID val="0">
324 <Volume channel="Master" val="50"/>
325 <Volume channel="LF" val="80"/>
326 <Volume channel="RF" val="85"/>
327 <Mute channel="LF" val="1"/>
328 </InstanceID>
329 </Event></LastChange>
330 </e:property>
331 </e:propertyset>"#;
332
333 let event = RenderingControlEvent::from_xml(xml).unwrap();
334 assert_eq!(event.master_volume(), Some("50".to_string()));
335 assert_eq!(event.lf_volume(), Some("80".to_string()));
336 assert_eq!(event.rf_volume(), Some("85".to_string()));
337 assert_eq!(event.lf_mute(), Some("1".to_string()));
338 }
339
340 #[test]
341 fn test_enriched_event_creation() {
342 let ip: IpAddr = "192.168.1.100".parse().unwrap();
343 let source = EventSource::UPnPNotification {
344 subscription_id: "uuid:123".to_string(),
345 };
346 let event_data = RenderingControlEvent {
347 property: RenderingControlProperty {
348 last_change: RenderingControlEventData {
349 instance: RenderingControlInstance {
350 volumes: vec![ChannelValueAttribute {
351 val: "50".to_string(),
352 channel: "Master".to_string(),
353 }],
354 mutes: vec![ChannelValueAttribute {
355 val: "0".to_string(),
356 channel: "Master".to_string(),
357 }],
358 bass: vec![],
359 treble: vec![],
360 loudness: vec![],
361 balance: vec![],
362 },
363 },
364 },
365 };
366
367 let enriched = create_enriched_event(ip, source, event_data);
368
369 assert_eq!(enriched.speaker_ip, ip);
370 assert_eq!(enriched.service, Service::RenderingControl);
371 assert!(enriched.registration_id.is_none());
372 }
373
374 #[test]
375 fn test_enriched_event_with_registration_id() {
376 let ip: IpAddr = "192.168.1.100".parse().unwrap();
377 let source = EventSource::UPnPNotification {
378 subscription_id: "uuid:123".to_string(),
379 };
380 let event_data = RenderingControlEvent {
381 property: RenderingControlProperty {
382 last_change: RenderingControlEventData {
383 instance: RenderingControlInstance {
384 volumes: vec![ChannelValueAttribute {
385 val: "50".to_string(),
386 channel: "Master".to_string(),
387 }],
388 mutes: vec![ChannelValueAttribute {
389 val: "0".to_string(),
390 channel: "Master".to_string(),
391 }],
392 bass: vec![],
393 treble: vec![],
394 loudness: vec![],
395 balance: vec![],
396 },
397 },
398 },
399 };
400
401 let enriched = create_enriched_event_with_registration_id(42, ip, source, event_data);
402
403 assert_eq!(enriched.registration_id, Some(42));
404 }
405
406 #[test]
407 fn test_lf_only_loudness_is_not_reported_as_master() {
408 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
409 <e:property>
410 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
411 <InstanceID val="0">
412 <Loudness channel="LF" val="1"/>
413 </InstanceID>
414 </Event></LastChange>
415 </e:property>
416 </e:propertyset>"#;
417
418 let event = RenderingControlEvent::from_xml(xml).unwrap();
419 assert_eq!(event.loudness(), None);
420 }
421
422 #[test]
423 fn test_duplicate_loudness_channels_do_not_drop_event() {
424 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
425 <e:property>
426 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
427 <InstanceID val="0">
428 <Volume channel="Master" val="42"/>
429 <Mute channel="Master" val="0"/>
430 <Loudness channel="Master" val="1"/>
431 <Loudness channel="LF" val="0"/>
432 </InstanceID>
433 </Event></LastChange>
434 </e:property>
435 </e:propertyset>"#;
436
437 let parsed = RenderingControlEvent::from_xml(xml);
438 assert!(
439 parsed.is_ok(),
440 "per-channel Loudness must not drop the event"
441 );
442
443 let event = parsed.unwrap();
444 assert_eq!(event.master_volume(), Some("42".to_string()));
446 assert_eq!(event.master_mute(), Some("0".to_string()));
447 assert_eq!(event.loudness(), Some("1".to_string()));
448 }
449
450 #[test]
451 fn test_scalar_bass_still_parses() {
452 let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
453 <e:property>
454 <LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
455 <InstanceID val="0">
456 <Bass val="2"/>
457 </InstanceID>
458 </Event></LastChange>
459 </e:property>
460 </e:propertyset>"#;
461
462 let event = RenderingControlEvent::from_xml(xml).unwrap();
463 assert_eq!(event.bass(), Some("2".to_string()));
464 }
465
466 #[test]
467 fn test_into_state_maps_all_fields() {
468 let event = RenderingControlEvent {
469 property: RenderingControlProperty {
470 last_change: RenderingControlEventData {
471 instance: RenderingControlInstance {
472 volumes: vec![
473 ChannelValueAttribute {
474 val: "50".to_string(),
475 channel: "Master".to_string(),
476 },
477 ChannelValueAttribute {
478 val: "45".to_string(),
479 channel: "LF".to_string(),
480 },
481 ChannelValueAttribute {
482 val: "55".to_string(),
483 channel: "RF".to_string(),
484 },
485 ],
486 mutes: vec![ChannelValueAttribute {
487 val: "0".to_string(),
488 channel: "Master".to_string(),
489 }],
490 bass: vec![master("5")],
491 treble: vec![master("-3")],
492 loudness: vec![master("1")],
493 balance: vec![],
494 },
495 },
496 },
497 };
498
499 let state = event.into_state();
500
501 assert_eq!(state.master_volume, Some("50".to_string()));
502 assert_eq!(state.master_mute, Some("0".to_string()));
503 assert_eq!(state.lf_volume, Some("45".to_string()));
504 assert_eq!(state.rf_volume, Some("55".to_string()));
505 assert_eq!(state.bass, Some("5".to_string()));
506 assert_eq!(state.treble, Some("-3".to_string()));
507 assert_eq!(state.loudness, Some("1".to_string()));
508 }
509}