Skip to main content

sonos_api/events/
types.rs

1//! Common event types and framework for Sonos UPnP events
2//!
3//! This module provides the core event infrastructure that is service-agnostic.
4//! Service-specific event types are defined in their respective service modules.
5
6use crate::{Result, Service};
7use serde::{Deserialize, Serialize};
8use std::net::IpAddr;
9use std::time::{Duration, SystemTime};
10
11/// An enriched event that includes context and source information
12#[derive(Debug, Clone)]
13pub struct EnrichedEvent<T> {
14    /// Registration ID this event belongs to (for sonos-stream integration)
15    pub registration_id: Option<u64>,
16
17    /// IP address of the speaker that generated this event
18    pub speaker_ip: IpAddr,
19
20    /// UPnP service that generated this event
21    pub service: Service,
22
23    /// Source of this event (UPnP notification or polling)
24    pub event_source: EventSource,
25
26    /// Timestamp when this event was processed
27    pub timestamp: SystemTime,
28
29    /// The actual service-specific event data
30    pub event_data: T,
31}
32
33impl<T> EnrichedEvent<T> {
34    /// Create a new enriched event
35    pub fn new(
36        speaker_ip: IpAddr,
37        service: Service,
38        event_source: EventSource,
39        event_data: T,
40    ) -> Self {
41        Self {
42            registration_id: None,
43            speaker_ip,
44            service,
45            event_source,
46            timestamp: SystemTime::now(),
47            event_data,
48        }
49    }
50
51    /// Create a new enriched event with registration ID (for sonos-stream integration)
52    pub fn with_registration_id(
53        registration_id: u64,
54        speaker_ip: IpAddr,
55        service: Service,
56        event_source: EventSource,
57        event_data: T,
58    ) -> Self {
59        Self {
60            registration_id: Some(registration_id),
61            speaker_ip,
62            service,
63            event_source,
64            timestamp: SystemTime::now(),
65            event_data,
66        }
67    }
68
69    /// Map the event data to a different type
70    pub fn map<U, F>(self, f: F) -> EnrichedEvent<U>
71    where
72        F: FnOnce(T) -> U,
73    {
74        EnrichedEvent {
75            registration_id: self.registration_id,
76            speaker_ip: self.speaker_ip,
77            service: self.service,
78            event_source: self.event_source,
79            timestamp: self.timestamp,
80            event_data: f(self.event_data),
81        }
82    }
83}
84
85/// Source of an event - indicates whether it came from UPnP events or polling
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub enum EventSource {
88    /// Event came from a UPnP NOTIFY message
89    UPnPNotification {
90        /// UPnP subscription ID
91        subscription_id: String,
92    },
93
94    /// Event was generated by polling device state
95    PollingDetection {
96        /// Current polling interval
97        poll_interval: Duration,
98    },
99
100    /// Event was generated during resync operation
101    ResyncOperation,
102}
103
104/// Trait for parsing service-specific events from XML
105pub trait EventParser: Send + Sync {
106    /// The event data type this parser produces
107    type EventData: Send + Sync + 'static;
108
109    /// Parse UPnP event XML and extract service-specific event data
110    fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData>;
111
112    /// Get the service type this parser handles
113    fn service_type(&self) -> Service;
114}
115
116/// Registry for service-specific event parsers
117#[derive(Default)]
118pub struct EventParserRegistry {
119    parsers: std::collections::HashMap<Service, Box<dyn EventParserDyn>>,
120}
121
122impl EventParserRegistry {
123    /// Create a new empty registry
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Register a parser for a specific service
129    pub fn register<P>(&mut self, parser: P)
130    where
131        P: EventParser + 'static,
132        P::EventData: 'static,
133    {
134        let service = parser.service_type();
135        self.parsers
136            .insert(service, Box::new(ParserWrapper::new(parser)));
137    }
138
139    /// Get a parser for a specific service
140    pub fn get_parser(&self, service: &Service) -> Option<&dyn EventParserDyn> {
141        self.parsers.get(service).map(|p| p.as_ref())
142    }
143
144    /// Check if a parser is registered for a service
145    pub fn has_parser(&self, service: &Service) -> bool {
146        self.parsers.contains_key(service)
147    }
148
149    /// Get all registered service types
150    pub fn supported_services(&self) -> Vec<Service> {
151        self.parsers.keys().cloned().collect()
152    }
153}
154
155/// Dynamic trait for type-erased event parsing
156pub trait EventParserDyn: Send + Sync {
157    /// Parse event XML to a dynamic event type
158    fn parse_upnp_event_dyn(&self, xml: &str) -> Result<Box<dyn std::any::Any + Send + Sync>>;
159
160    /// Get the service type
161    fn service_type(&self) -> Service;
162}
163
164/// Wrapper to convert typed EventParser to dynamic EventParserDyn
165struct ParserWrapper<P> {
166    parser: P,
167}
168
169impl<P> ParserWrapper<P>
170where
171    P: EventParser,
172    P::EventData: 'static,
173{
174    fn new(parser: P) -> Self {
175        Self { parser }
176    }
177}
178
179impl<P> EventParserDyn for ParserWrapper<P>
180where
181    P: EventParser + Send + Sync,
182    P::EventData: Send + Sync + 'static,
183{
184    fn parse_upnp_event_dyn(&self, xml: &str) -> Result<Box<dyn std::any::Any + Send + Sync>> {
185        let event_data = self.parser.parse_upnp_event(xml)?;
186        Ok(Box::new(event_data))
187    }
188
189    fn service_type(&self) -> Service {
190        self.parser.service_type()
191    }
192}
193
194/// Helper function to extract values from XML using basic text matching
195/// This is used as a fallback when proper parsers are not available
196pub fn extract_xml_value(xml: &str, tag: &str) -> Option<String> {
197    let start_tag = format!("<{tag}>");
198    let end_tag = format!("</{tag}>");
199
200    if let Some(start_pos) = xml.find(&start_tag) {
201        let content_start = start_pos + start_tag.len();
202        if let Some(end_pos) = xml[content_start..].find(&end_tag) {
203            let value = xml[content_start..content_start + end_pos].trim();
204            if !value.is_empty() {
205                return Some(value.to_string());
206            }
207        }
208    }
209    None
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[derive(Debug, Clone, PartialEq)]
217    struct TestEventData {
218        value: String,
219    }
220
221    struct TestParser;
222
223    impl EventParser for TestParser {
224        type EventData = TestEventData;
225
226        fn parse_upnp_event(&self, xml: &str) -> Result<Self::EventData> {
227            Ok(TestEventData {
228                value: xml.to_string(),
229            })
230        }
231
232        fn service_type(&self) -> Service {
233            Service::AVTransport
234        }
235    }
236
237    #[test]
238    fn test_enriched_event_creation() {
239        let ip: IpAddr = "192.168.1.100".parse().unwrap();
240        let source = EventSource::UPnPNotification {
241            subscription_id: "uuid:123".to_string(),
242        };
243        let data = TestEventData {
244            value: "test".to_string(),
245        };
246
247        let event = EnrichedEvent::new(ip, Service::AVTransport, source, data.clone());
248
249        assert_eq!(event.speaker_ip, ip);
250        assert_eq!(event.service, Service::AVTransport);
251        assert_eq!(event.event_data, data);
252        assert!(event.registration_id.is_none());
253    }
254
255    #[test]
256    fn test_enriched_event_with_registration_id() {
257        let ip: IpAddr = "192.168.1.100".parse().unwrap();
258        let source = EventSource::UPnPNotification {
259            subscription_id: "uuid:123".to_string(),
260        };
261        let data = TestEventData {
262            value: "test".to_string(),
263        };
264
265        let event =
266            EnrichedEvent::with_registration_id(42, ip, Service::AVTransport, source, data.clone());
267
268        assert_eq!(event.registration_id, Some(42));
269        assert_eq!(event.event_data, data);
270    }
271
272    #[test]
273    fn test_event_mapping() {
274        let ip: IpAddr = "192.168.1.100".parse().unwrap();
275        let source = EventSource::UPnPNotification {
276            subscription_id: "uuid:123".to_string(),
277        };
278        let data = TestEventData {
279            value: "test".to_string(),
280        };
281
282        let event = EnrichedEvent::new(ip, Service::AVTransport, source, data);
283        let mapped_event = event.map(|data| data.value.len());
284
285        assert_eq!(mapped_event.event_data, 4); // "test".len()
286    }
287
288    #[test]
289    fn test_parser_registry() {
290        let mut registry = EventParserRegistry::new();
291        assert!(!registry.has_parser(&Service::AVTransport));
292
293        registry.register(TestParser);
294        assert!(registry.has_parser(&Service::AVTransport));
295
296        let supported = registry.supported_services();
297        assert!(supported.contains(&Service::AVTransport));
298    }
299
300    #[test]
301    fn test_xml_value_extraction() {
302        let xml = "<Test>value</Test>";
303        assert_eq!(extract_xml_value(xml, "Test"), Some("value".to_string()));
304
305        let xml = "<Test></Test>";
306        assert_eq!(extract_xml_value(xml, "Test"), None);
307
308        let xml = "<Other>value</Other>";
309        assert_eq!(extract_xml_value(xml, "Test"), None);
310    }
311}