rupnp/
device.rs

1use crate::{
2    find_in_xml,
3    utils::{self, HttpResponseExt, HyperBodyExt},
4    Result, Service,
5};
6use bytes::Bytes;
7use http::Uri;
8use http_body_util::Empty;
9use hyper_util::rt::TokioExecutor;
10use roxmltree::{Document, Node};
11use ssdp_client::URN;
12use std::collections::HashMap;
13use std::hash::Hash;
14use std::hash::Hasher;
15
16#[derive(Debug, Clone)]
17/// A UPnP Device.
18/// It stores its [`Uri`] and a [`DeviceSpec`], which contains information like the device type and
19/// its list of inner devices and services.
20pub struct Device {
21    url: Uri,
22    device_spec: DeviceSpec,
23}
24impl Device {
25    pub fn url(&self) -> &Uri {
26        &self.url
27    }
28
29    /// Creates a UPnP device from the given url.
30    /// The url should point to the `/device_description.xml` or similar of the device.
31    /// If you dont know the concrete location, use [`discover`](fn.discover.html) instead.
32    pub async fn from_url(url: Uri) -> Result<Self> {
33        Self::from_url_and_properties(url, &[]).await
34    }
35
36    /// Creates a UPnP device from the given url, defining extra device properties
37    /// to be accessed with `get_extra_property`.
38    pub async fn from_url_and_properties(url: Uri, extra_keys: &[&str]) -> Result<Self> {
39        let body = hyper_util::client::legacy::Client::builder(TokioExecutor::new())
40            .build_http::<Empty<Bytes>>()
41            .get(url.clone())
42            .await?
43            .err_if_not_200()?
44            .into_body()
45            .bytes()
46            .await?;
47
48        // .into_body()
49        // .text()
50        // .await?;
51        let body = std::str::from_utf8(&body)?;
52
53        let document = Document::parse(body)?;
54        let device = utils::find_root(&document, "device", "Device Description")?;
55        let device_spec = DeviceSpec::from_xml(device, extra_keys)?;
56
57        Ok(Self { url, device_spec })
58    }
59}
60impl std::ops::Deref for Device {
61    type Target = DeviceSpec;
62
63    fn deref(&self) -> &Self::Target {
64        &self.device_spec
65    }
66}
67impl Hash for Device {
68    fn hash<H: Hasher>(&self, state: &mut H) {
69        self.url.hash(state);
70    }
71}
72impl PartialEq for Device {
73    fn eq(&self, other: &Self) -> bool {
74        self.url == other.url
75    }
76}
77impl Eq for Device {}
78
79/// Information about a device.
80///
81/// By default it only includes its *friendly name*, device type, a list of subdevices and
82/// services, and a `HashMap` of extra properties in order to keep the structs size small.
83///
84/// If you also want the `ManufacturerURL`, `Model{Description,Number,Url}`, `serial number`, `UDN` and
85/// `UPC` as struct fields, enable the `full_device_spec` feature.
86#[derive(Debug, Clone)]
87pub struct DeviceSpec {
88    device_type: URN,
89    friendly_name: String,
90
91    devices: Vec<DeviceSpec>,
92    services: Vec<Service>,
93
94    extra_properties: HashMap<String, Option<String>>,
95
96    #[cfg(feature = "full_device_spec")]
97    manufacturer: String,
98    #[cfg(feature = "full_device_spec")]
99    manufacturer_url: Option<String>,
100    #[cfg(feature = "full_device_spec")]
101    model_name: String,
102    #[cfg(feature = "full_device_spec")]
103    model_description: Option<String>,
104    #[cfg(feature = "full_device_spec")]
105    model_number: Option<String>,
106    #[cfg(feature = "full_device_spec")]
107    model_url: Option<String>,
108    #[cfg(feature = "full_device_spec")]
109    serial_number: Option<String>,
110    #[cfg(feature = "full_device_spec")]
111    udn: String,
112    #[cfg(feature = "full_device_spec")]
113    upc: Option<String>,
114    #[cfg(feature = "full_device_spec")]
115    presentation_url: Option<String>,
116}
117
118impl DeviceSpec {
119    fn from_xml<'a, 'input: 'a>(node: Node<'a, 'input>, extra_keys: &[&str]) -> Result<Self> {
120        #[rustfmt::skip]
121        #[allow(non_snake_case)]
122        let (device_type, friendly_name, services, devices, extra_properties) =
123            find_in_xml! { node => deviceType, friendlyName, ?serviceList, ?deviceList, #extra_keys };
124
125        #[cfg(feature = "full_device_spec")]
126        #[allow(non_snake_case)]
127        let (
128            manufacturer,
129            manufacturer_url,
130            model_name,
131            model_description,
132            model_number,
133            model_url,
134            serial_number,
135            udn,
136            upc,
137            presentation_url,
138        ) = find_in_xml! { node => manufacturer, ?manufacturerURL, modelName, ?modelDescription, ?modelNumber, ?modelURL, ?serialNumber, UDN, ?UPC, ?PresentationURL};
139
140        #[cfg(feature = "full_device_spec")]
141        let manufacturer_url = manufacturer_url.map(utils::parse_node_text).transpose()?;
142        #[cfg(feature = "full_device_spec")]
143        let model_description = model_description.map(utils::parse_node_text).transpose()?;
144        #[cfg(feature = "full_device_spec")]
145        let model_number = model_number.map(utils::parse_node_text).transpose()?;
146        #[cfg(feature = "full_device_spec")]
147        let model_url = model_url.map(utils::parse_node_text).transpose()?;
148        #[cfg(feature = "full_device_spec")]
149        let serial_number = serial_number.map(utils::parse_node_text).transpose()?;
150        #[cfg(feature = "full_device_spec")]
151        let upc = upc.map(utils::parse_node_text).transpose()?;
152        #[cfg(feature = "full_device_spec")]
153        let presentation_url = presentation_url.map(utils::parse_node_text).transpose()?;
154
155        let devices = match devices {
156            Some(d) => d
157                .children()
158                .filter(Node::is_element)
159                .map(|node| DeviceSpec::from_xml(node, extra_keys))
160                .collect::<Result<_>>()?,
161            None => Vec::new(),
162        };
163        let services = match services {
164            Some(s) => s
165                .children()
166                .filter(Node::is_element)
167                .map(Service::from_xml)
168                .collect::<Result<_>>()?,
169            None => Vec::new(),
170        };
171
172        Ok(Self {
173            device_type: utils::parse_node_text(device_type)?,
174            friendly_name: utils::parse_node_text(friendly_name)?,
175            #[cfg(feature = "full_device_spec")]
176            manufacturer: utils::parse_node_text(manufacturer)?,
177            #[cfg(feature = "full_device_spec")]
178            udn: utils::parse_node_text(udn)?,
179            #[cfg(feature = "full_device_spec")]
180            manufacturer_url,
181            #[cfg(feature = "full_device_spec")]
182            model_name: utils::parse_node_text(model_name)?,
183            #[cfg(feature = "full_device_spec")]
184            model_description,
185            #[cfg(feature = "full_device_spec")]
186            model_number,
187            #[cfg(feature = "full_device_spec")]
188            model_url,
189            #[cfg(feature = "full_device_spec")]
190            serial_number,
191            #[cfg(feature = "full_device_spec")]
192            upc,
193            #[cfg(feature = "full_device_spec")]
194            presentation_url,
195            devices,
196            services,
197            extra_properties,
198        })
199    }
200
201    pub fn device_type(&self) -> &URN {
202        &self.device_type
203    }
204    pub fn friendly_name(&self) -> &str {
205        &self.friendly_name
206    }
207    pub fn get_extra_property(&self, elem: &str) -> Option<&str> {
208        self.extra_properties
209            .get(elem)
210            .and_then(|o| o.as_ref())
211            .map(String::as_str)
212    }
213
214    #[cfg(feature = "full_device_spec")]
215    pub fn manufacturer(&self) -> &str {
216        &self.manufacturer
217    }
218    #[cfg(feature = "full_device_spec")]
219    pub fn manufacturer_url(&self) -> Option<&str> {
220        self.manufacturer_url.as_ref().map(String::as_str)
221    }
222    #[cfg(feature = "full_device_spec")]
223    pub fn model_name(&self) -> &str {
224        &self.model_name
225    }
226    #[cfg(feature = "full_device_spec")]
227    pub fn model_description(&self) -> Option<&str> {
228        self.model_description.as_ref().map(String::as_str)
229    }
230    #[cfg(feature = "full_device_spec")]
231    pub fn model_number(&self) -> Option<&str> {
232        self.model_number.as_ref().map(String::as_str)
233    }
234    #[cfg(feature = "full_device_spec")]
235    pub fn model_url(&self) -> Option<&str> {
236        self.model_url.as_ref().map(String::as_str)
237    }
238    #[cfg(feature = "full_device_spec")]
239    pub fn serial_number(&self) -> Option<&str> {
240        self.serial_number.as_ref().map(String::as_str)
241    }
242    #[cfg(feature = "full_device_spec")]
243    pub fn udn(&self) -> &str {
244        &self.udn
245    }
246    #[cfg(feature = "full_device_spec")]
247    pub fn upc(&self) -> Option<&str> {
248        self.upc.as_ref().map(String::as_str)
249    }
250
251    /// Returns a list of this devices subdevices.
252    /// Note that this does not recurse, if you want that behaviour use
253    /// [devices_iter](struct.DeviceSpec.html#method.devices_iter) instead.
254    pub fn devices(&self) -> &Vec<DeviceSpec> {
255        &self.devices
256    }
257
258    /// Returns a list of this devices services.
259    /// Note that this does not recurse, if you want that behaviour use
260    /// [services_iter](struct.DeviceSpec.html#method.services_iter) instead.
261    pub fn services(&self) -> &Vec<Service> {
262        &self.services
263    }
264
265    /// Returns an Iterator of all services that can be used from this device.
266    pub fn services_iter(&self) -> impl Iterator<Item = &Service> {
267        self.services().iter().chain(self.devices().iter().flat_map(
268            |device| -> Box<dyn Iterator<Item = &Service>> { Box::new(device.services_iter()) },
269        ))
270    }
271    pub fn find_service(&self, service_type: &URN) -> Option<&Service> {
272        self.services_iter()
273            .find(|s| s.service_type() == service_type)
274    }
275
276    /// Returns an Iterator of all devices that can be used from this device.
277    pub fn devices_iter(&self) -> impl Iterator<Item = &DeviceSpec> {
278        self.devices().iter().chain(self.devices().iter().flat_map(
279            |device| -> Box<dyn Iterator<Item = &DeviceSpec>> { Box::new(device.devices_iter()) },
280        ))
281    }
282    pub fn find_device(&self, device_type: &URN) -> Option<&DeviceSpec> {
283        self.devices_iter().find(|d| &d.device_type == device_type)
284    }
285}