use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use crate::events::{xml_utils, EnrichedEvent, EventParser, EventSource};
use crate::{ApiError, Result, Service};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "propertyset")]
pub struct RenderingControlEvent {
#[serde(rename = "property")]
property: RenderingControlProperty,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RenderingControlProperty {
#[serde(
rename = "LastChange",
deserialize_with = "xml_utils::deserialize_nested"
)]
last_change: RenderingControlEventData,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename = "Event")]
pub struct RenderingControlEventData {
#[serde(rename = "InstanceID")]
instance: RenderingControlInstance,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct RenderingControlInstance {
#[serde(rename = "Volume", default)]
pub volumes: Vec<ChannelValueAttribute>,
#[serde(rename = "Mute", default)]
pub mutes: Vec<ChannelValueAttribute>,
#[serde(rename = "Bass", default)]
pub bass: Option<xml_utils::ValueAttribute>,
#[serde(rename = "Treble", default)]
pub treble: Option<xml_utils::ValueAttribute>,
#[serde(rename = "Loudness", default)]
pub loudness: Option<xml_utils::ValueAttribute>,
#[serde(rename = "Balance", default)]
pub balance: Option<xml_utils::ValueAttribute>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ChannelValueAttribute {
#[serde(rename = "@val", default)]
pub val: String,
#[serde(rename = "@channel", default)]
pub channel: String,
}
impl RenderingControlEvent {
pub fn master_volume(&self) -> Option<String> {
self.get_volume_for_channel("Master")
}
pub fn lf_volume(&self) -> Option<String> {
self.get_volume_for_channel("LF")
}
pub fn rf_volume(&self) -> Option<String> {
self.get_volume_for_channel("RF")
}
pub fn master_mute(&self) -> Option<String> {
self.get_mute_for_channel("Master")
}
pub fn lf_mute(&self) -> Option<String> {
self.get_mute_for_channel("LF")
}
pub fn rf_mute(&self) -> Option<String> {
self.get_mute_for_channel("RF")
}
pub fn bass(&self) -> Option<String> {
self.property
.last_change
.instance
.bass
.as_ref()
.map(|v| v.val.clone())
}
pub fn treble(&self) -> Option<String> {
self.property
.last_change
.instance
.treble
.as_ref()
.map(|v| v.val.clone())
}
pub fn loudness(&self) -> Option<String> {
self.property
.last_change
.instance
.loudness
.as_ref()
.map(|v| v.val.clone())
}
pub fn balance(&self) -> Option<String> {
self.property
.last_change
.instance
.balance
.as_ref()
.map(|v| v.val.clone())
}
pub fn other_channels(&self) -> HashMap<String, String> {
let mut channels = HashMap::new();
for volume in &self.property.last_change.instance.volumes {
if !["Master", "LF", "RF"].contains(&volume.channel.as_str()) {
channels.insert(format!("{}Volume", volume.channel), volume.val.clone());
}
}
for mute in &self.property.last_change.instance.mutes {
if !["Master", "LF", "RF"].contains(&mute.channel.as_str()) {
channels.insert(format!("{}Mute", mute.channel), mute.val.clone());
}
}
channels
}
fn get_volume_for_channel(&self, channel: &str) -> Option<String> {
self.property
.last_change
.instance
.volumes
.iter()
.find(|v| v.channel == channel)
.map(|v| v.val.clone())
}
fn get_mute_for_channel(&self, channel: &str) -> Option<String> {
self.property
.last_change
.instance
.mutes
.iter()
.find(|m| m.channel == channel)
.map(|m| m.val.clone())
}
pub fn into_state(&self) -> super::state::RenderingControlState {
super::state::RenderingControlState {
master_volume: self.master_volume(),
master_mute: self.master_mute(),
lf_volume: self.lf_volume(),
rf_volume: self.rf_volume(),
lf_mute: self.lf_mute(),
rf_mute: self.rf_mute(),
bass: self.bass(),
treble: self.treble(),
loudness: self.loudness(),
balance: self.balance(),
other_channels: self.other_channels(),
}
}
pub fn from_xml(xml: &str) -> Result<Self> {
let clean_xml = xml_utils::strip_namespaces(xml);
quick_xml::de::from_str(&clean_xml)
.map_err(|e| ApiError::ParseError(format!("Failed to parse RenderingControl XML: {e}")))
}
}
pub struct RenderingControlEventParser;
impl EventParser for RenderingControlEventParser {
type EventData = RenderingControlEvent;
fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData> {
RenderingControlEvent::from_xml(xml)
}
fn service_type(&self) -> Service {
Service::RenderingControl
}
}
pub fn create_enriched_event(
speaker_ip: IpAddr,
event_source: EventSource,
event_data: RenderingControlEvent,
) -> EnrichedEvent<RenderingControlEvent> {
EnrichedEvent::new(
speaker_ip,
Service::RenderingControl,
event_source,
event_data,
)
}
pub fn create_enriched_event_with_registration_id(
registration_id: u64,
speaker_ip: IpAddr,
event_source: EventSource,
event_data: RenderingControlEvent,
) -> EnrichedEvent<RenderingControlEvent> {
EnrichedEvent::with_registration_id(
registration_id,
speaker_ip,
Service::RenderingControl,
event_source,
event_data,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rendering_control_parser_service_type() {
let parser = RenderingControlEventParser;
assert_eq!(parser.service_type(), Service::RenderingControl);
}
#[test]
fn test_rendering_control_event_creation() {
let event = RenderingControlEvent {
property: RenderingControlProperty {
last_change: RenderingControlEventData {
instance: RenderingControlInstance {
volumes: vec![ChannelValueAttribute {
val: "75".to_string(),
channel: "Master".to_string(),
}],
mutes: vec![ChannelValueAttribute {
val: "false".to_string(),
channel: "Master".to_string(),
}],
bass: Some(xml_utils::ValueAttribute {
val: "0".to_string(),
}),
treble: Some(xml_utils::ValueAttribute {
val: "0".to_string(),
}),
loudness: Some(xml_utils::ValueAttribute {
val: "true".to_string(),
}),
balance: Some(xml_utils::ValueAttribute {
val: "0".to_string(),
}),
},
},
},
};
assert_eq!(event.master_volume(), Some("75".to_string()));
assert_eq!(event.master_mute(), Some("false".to_string()));
assert!(event.other_channels().is_empty());
}
#[test]
fn test_basic_xml_parsing() {
let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
<e:property>
<LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
<InstanceID val="0">
<Volume channel="Master" val="75"/>
<Mute channel="Master" val="0"/>
<Bass val="2"/>
<Treble val="-1"/>
</InstanceID>
</Event></LastChange>
</e:property>
</e:propertyset>"#;
let event = RenderingControlEvent::from_xml(xml).unwrap();
assert_eq!(event.master_volume(), Some("75".to_string()));
assert_eq!(event.master_mute(), Some("0".to_string()));
assert_eq!(event.bass(), Some("2".to_string()));
assert_eq!(event.treble(), Some("-1".to_string()));
}
#[test]
fn test_channel_specific_volume() {
let xml = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
<e:property>
<LastChange><Event xmlns="urn:schemas-upnp-org:metadata-1-0/RCS/">
<InstanceID val="0">
<Volume channel="Master" val="50"/>
<Volume channel="LF" val="80"/>
<Volume channel="RF" val="85"/>
<Mute channel="LF" val="1"/>
</InstanceID>
</Event></LastChange>
</e:property>
</e:propertyset>"#;
let event = RenderingControlEvent::from_xml(xml).unwrap();
assert_eq!(event.master_volume(), Some("50".to_string()));
assert_eq!(event.lf_volume(), Some("80".to_string()));
assert_eq!(event.rf_volume(), Some("85".to_string()));
assert_eq!(event.lf_mute(), Some("1".to_string()));
}
#[test]
fn test_enriched_event_creation() {
let ip: IpAddr = "192.168.1.100".parse().unwrap();
let source = EventSource::UPnPNotification {
subscription_id: "uuid:123".to_string(),
};
let event_data = RenderingControlEvent {
property: RenderingControlProperty {
last_change: RenderingControlEventData {
instance: RenderingControlInstance {
volumes: vec![ChannelValueAttribute {
val: "50".to_string(),
channel: "Master".to_string(),
}],
mutes: vec![ChannelValueAttribute {
val: "0".to_string(),
channel: "Master".to_string(),
}],
bass: None,
treble: None,
loudness: None,
balance: None,
},
},
},
};
let enriched = create_enriched_event(ip, source, event_data);
assert_eq!(enriched.speaker_ip, ip);
assert_eq!(enriched.service, Service::RenderingControl);
assert!(enriched.registration_id.is_none());
}
#[test]
fn test_enriched_event_with_registration_id() {
let ip: IpAddr = "192.168.1.100".parse().unwrap();
let source = EventSource::UPnPNotification {
subscription_id: "uuid:123".to_string(),
};
let event_data = RenderingControlEvent {
property: RenderingControlProperty {
last_change: RenderingControlEventData {
instance: RenderingControlInstance {
volumes: vec![ChannelValueAttribute {
val: "50".to_string(),
channel: "Master".to_string(),
}],
mutes: vec![ChannelValueAttribute {
val: "0".to_string(),
channel: "Master".to_string(),
}],
bass: None,
treble: None,
loudness: None,
balance: None,
},
},
},
};
let enriched = create_enriched_event_with_registration_id(42, ip, source, event_data);
assert_eq!(enriched.registration_id, Some(42));
}
#[test]
fn test_into_state_maps_all_fields() {
let event = RenderingControlEvent {
property: RenderingControlProperty {
last_change: RenderingControlEventData {
instance: RenderingControlInstance {
volumes: vec![
ChannelValueAttribute {
val: "50".to_string(),
channel: "Master".to_string(),
},
ChannelValueAttribute {
val: "45".to_string(),
channel: "LF".to_string(),
},
ChannelValueAttribute {
val: "55".to_string(),
channel: "RF".to_string(),
},
],
mutes: vec![ChannelValueAttribute {
val: "0".to_string(),
channel: "Master".to_string(),
}],
bass: Some(xml_utils::ValueAttribute {
val: "5".to_string(),
}),
treble: Some(xml_utils::ValueAttribute {
val: "-3".to_string(),
}),
loudness: Some(xml_utils::ValueAttribute {
val: "1".to_string(),
}),
balance: None,
},
},
},
};
let state = event.into_state();
assert_eq!(state.master_volume, Some("50".to_string()));
assert_eq!(state.master_mute, Some("0".to_string()));
assert_eq!(state.lf_volume, Some("45".to_string()));
assert_eq!(state.rf_volume, Some("55".to_string()));
assert_eq!(state.bass, Some("5".to_string()));
assert_eq!(state.treble, Some("-3".to_string()));
assert_eq!(state.loudness, Some("1".to_string()));
}
}