Skip to main content

sonos_stream/events/
types.rs

1//! Event types for the sonos-stream crate
2//!
3//! This module defines the event envelope types (EnrichedEvent, EventSource, EventData)
4//! and re-exports canonical state types from sonos-api. The actual per-service state
5//! structs live in sonos-api; sonos-stream wraps them in EventData for transport.
6
7use serde::{Deserialize, Serialize};
8use std::net::IpAddr;
9use std::time::{Duration, SystemTime};
10
11use crate::registry::RegistrationId;
12
13// Re-export sonos-api state types for convenience
14pub use sonos_api::services::av_transport::state::AVTransportState;
15pub use sonos_api::services::group_management::state::GroupManagementState;
16pub use sonos_api::services::group_rendering_control::state::GroupRenderingControlState;
17pub use sonos_api::services::rendering_control::state::RenderingControlState;
18pub use sonos_api::services::zone_group_topology::state::ZoneGroupTopologyState;
19
20// Re-export topology sub-types used by consumers (e.g. sonos-state decoder tests)
21pub use sonos_api::services::zone_group_topology::events::{
22    NetworkInfo, SatelliteInfo, ZoneGroupInfo, ZoneGroupMemberInfo,
23};
24
25/// An enriched event that includes context and source information
26#[derive(Debug, Clone)]
27pub struct EnrichedEvent {
28    /// Registration ID this event belongs to
29    pub registration_id: RegistrationId,
30
31    /// IP address of the speaker that generated this event
32    pub speaker_ip: IpAddr,
33
34    /// UPnP service that generated this event
35    pub service: sonos_api::Service,
36
37    /// Source of this event (UPnP notification or polling)
38    pub event_source: EventSource,
39
40    /// Timestamp when this event was processed
41    pub timestamp: SystemTime,
42
43    /// The actual event data
44    pub event_data: EventData,
45}
46
47impl EnrichedEvent {
48    /// Create a new enriched event
49    pub fn new(
50        registration_id: RegistrationId,
51        speaker_ip: IpAddr,
52        service: sonos_api::Service,
53        event_source: EventSource,
54        event_data: EventData,
55    ) -> Self {
56        Self {
57            registration_id,
58            speaker_ip,
59            service,
60            event_source,
61            timestamp: SystemTime::now(),
62            event_data,
63        }
64    }
65}
66
67/// Source of an event - indicates whether it came from UPnP events or polling
68#[derive(Debug, Clone)]
69pub enum EventSource {
70    /// Event came from a UPnP NOTIFY message
71    UPnPNotification {
72        /// UPnP subscription ID
73        subscription_id: String,
74    },
75
76    /// Event was generated by polling device state
77    PollingDetection {
78        /// Current polling interval
79        poll_interval: Duration,
80    },
81}
82
83/// Event data - complete event information for each service.
84///
85/// Variants reference canonical State types from sonos-api.
86/// Both UPnP events (via `into_state()`) and polling (via `poll()`)
87/// produce the same State types, ensuring parity.
88#[derive(Debug, Clone)]
89pub enum EventData {
90    /// AVTransport service state
91    AVTransport(AVTransportState),
92
93    /// RenderingControl service state
94    RenderingControl(RenderingControlState),
95
96    /// DeviceProperties service event (no sonos-api State type yet)
97    DeviceProperties(DevicePropertiesEvent),
98
99    /// ZoneGroupTopology service state
100    ZoneGroupTopology(ZoneGroupTopologyState),
101
102    /// GroupManagement service state
103    GroupManagement(GroupManagementState),
104
105    /// GroupRenderingControl service state
106    GroupRenderingControl(GroupRenderingControlState),
107}
108
109impl EventData {
110    /// Get the service type for this event data
111    pub fn service_type(&self) -> sonos_api::Service {
112        match self {
113            EventData::AVTransport(_) => sonos_api::Service::AVTransport,
114            EventData::RenderingControl(_) => sonos_api::Service::RenderingControl,
115            EventData::DeviceProperties(_) => {
116                // FIXME: DeviceProperties needs its own Service variant in sonos-api.
117                // Using ZoneGroupTopology as fallback could cause misrouted events
118                // if DeviceProperties polling is ever added to the scheduler.
119                sonos_api::Service::ZoneGroupTopology
120            }
121            EventData::ZoneGroupTopology(_) => sonos_api::Service::ZoneGroupTopology,
122            EventData::GroupManagement(_) => sonos_api::Service::GroupManagement,
123            EventData::GroupRenderingControl(_) => sonos_api::Service::GroupRenderingControl,
124        }
125    }
126}
127
128// DeviceProperties event types — kept here since there's no sonos-api State type yet
129
130/// Complete DeviceProperties event data containing all device property information
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct DevicePropertiesEvent {
133    /// Current zone name
134    pub zone_name: Option<String>,
135
136    /// Current zone icon
137    pub zone_icon: Option<String>,
138
139    /// Current configuration information
140    pub configuration: Option<String>,
141
142    /// Device capabilities
143    pub capabilities: Option<String>,
144
145    /// Firmware version
146    pub software_version: Option<String>,
147
148    /// Device model information
149    pub model_name: Option<String>,
150
151    /// Device display version
152    pub display_version: Option<String>,
153
154    /// Device hardware version
155    pub hardware_version: Option<String>,
156
157    /// Additional device properties (extensible)
158    pub additional_properties: std::collections::HashMap<String, String>,
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_enriched_event_creation() {
167        let reg_id = RegistrationId::new(1);
168        let ip: IpAddr = "192.168.1.100".parse().unwrap();
169        let service = sonos_api::Service::AVTransport;
170        let source = EventSource::UPnPNotification {
171            subscription_id: "uuid:123".to_string(),
172        };
173        let data = EventData::AVTransport(AVTransportState {
174            transport_state: Some("PLAYING".to_string()),
175            transport_status: None,
176            speed: None,
177            current_track_uri: None,
178            track_duration: None,
179            track_metadata: None,
180            rel_time: None,
181            abs_time: None,
182            rel_count: None,
183            abs_count: None,
184            play_mode: None,
185            next_track_uri: None,
186            next_track_metadata: None,
187            queue_length: None,
188        });
189
190        let event = EnrichedEvent::new(reg_id, ip, service, source, data);
191
192        assert_eq!(event.registration_id, reg_id);
193        assert_eq!(event.speaker_ip, ip);
194        assert_eq!(event.service, service);
195    }
196
197    #[test]
198    fn test_event_data_service_type() {
199        let av_event = EventData::AVTransport(AVTransportState {
200            transport_state: Some("PLAYING".to_string()),
201            transport_status: None,
202            speed: None,
203            current_track_uri: None,
204            track_duration: None,
205            track_metadata: None,
206            rel_time: None,
207            abs_time: None,
208            rel_count: None,
209            abs_count: None,
210            play_mode: None,
211            next_track_uri: None,
212            next_track_metadata: None,
213            queue_length: None,
214        });
215        assert_eq!(av_event.service_type(), sonos_api::Service::AVTransport);
216
217        let rc_event = EventData::RenderingControl(RenderingControlState {
218            master_volume: Some("50".to_string()),
219            master_mute: Some("false".to_string()),
220            lf_volume: None,
221            rf_volume: None,
222            lf_mute: None,
223            rf_mute: None,
224            bass: None,
225            treble: None,
226            loudness: None,
227            balance: None,
228            other_channels: std::collections::HashMap::new(),
229        });
230        assert_eq!(
231            rc_event.service_type(),
232            sonos_api::Service::RenderingControl
233        );
234
235        let gm_event = EventData::GroupManagement(GroupManagementState {
236            group_coordinator_is_local: Some(true),
237            local_group_uuid: None,
238            reset_volume_after: None,
239            virtual_line_in_group_id: None,
240            volume_av_transport_uri: None,
241        });
242        assert_eq!(gm_event.service_type(), sonos_api::Service::GroupManagement);
243
244        let grc_event = EventData::GroupRenderingControl(GroupRenderingControlState {
245            group_volume: Some(14),
246            group_mute: Some(false),
247            group_volume_changeable: Some(true),
248        });
249        assert_eq!(
250            grc_event.service_type(),
251            sonos_api::Service::GroupRenderingControl
252        );
253    }
254}