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)]
17pub struct Device {
21 url: Uri,
22 device_spec: DeviceSpec,
23}
24impl Device {
25 pub fn url(&self) -> &Uri {
26 &self.url
27 }
28
29 pub async fn from_url(url: Uri) -> Result<Self> {
33 Self::from_url_and_properties(url, &[]).await
34 }
35
36 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 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#[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 pub fn devices(&self) -> &Vec<DeviceSpec> {
255 &self.devices
256 }
257
258 pub fn services(&self) -> &Vec<Service> {
262 &self.services
263 }
264
265 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 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}