rusty_sonos/
discovery.rs

1//! Resources for learning about speakers on the current network
2
3use std::{
4    net::{IpAddr, Ipv4Addr, UdpSocket},
5    time::{Duration, Instant},
6};
7
8use reqwest::StatusCode;
9
10use crate::{
11    errors::{SonosError, SpeakerError, UDPError},
12    speaker::BasicSpeakerInfo,
13    xml::{get_error_code, parse_description_xml},
14};
15
16const DISCOVERY_REQUEST_BODY: &str = "M-SEARCH * HTTP/1.1
17HOST: 239.255.255.250:1900
18MAN: ssdp:discover
19MX: 1
20ST: urn:schemas-upnp-org:device:ZonePlayer:1";
21
22const DESCRIPTION_ENDPOINT: &str = "/xml/device_description.xml";
23
24/// Returns basic information about a speaker, if one is found at the given IP address
25/// * `ip_addr` - the IP of the speaker to query for information
26pub async fn get_speaker_info(ip_addr: Ipv4Addr) -> Result<BasicSpeakerInfo, SpeakerError> {
27    let url = format!("http://{}:1400{}", ip_addr, DESCRIPTION_ENDPOINT);
28
29    let response = reqwest::get(&url).await?;
30
31    let status = response.status();
32    let xml_response = response.text().await?;
33
34    if let StatusCode::OK = status {
35        let speaker_info = parse_description_xml(xml_response, ip_addr)?;
36
37        Ok(speaker_info)
38    } else {
39        let error_code = get_error_code(xml_response)?;
40
41        Err(SpeakerError::from(SonosError::from_err_code(
42            &error_code,
43            &format!("HTTP status code: {}", status),
44        )))
45    }
46}
47
48/// Returns devices discovered on the current network within a given amount of time
49/// * `search_timeout` - how long the function will accept responses from speakers (the function will return in about this many seconds)
50/// * `read_timeout` - the maximum amount of time for which the function will try and read data from a given response
51pub async fn discover_devices(
52    search_timeout: Duration,
53    read_timeout: Duration,
54) -> Result<Vec<BasicSpeakerInfo>, UDPError> {
55    let socket: UdpSocket = UdpSocket::bind("0.0.0.0:0")?;
56
57    socket.set_broadcast(true)?;
58
59    socket.set_read_timeout(Some(read_timeout))?;
60
61    socket.send_to(DISCOVERY_REQUEST_BODY.as_bytes(), "239.255.255.250:1900")?;
62
63    socket.send_to(DISCOVERY_REQUEST_BODY.as_bytes(), "255.255.255.255:1900")?;
64
65    let start_time = Instant::now();
66
67    // this buffer is large enough to hold typical speaker response
68    let mut buf = [0; 1024];
69
70    let mut discovered_speakers = Vec::new();
71
72    loop {
73        if start_time.elapsed() > search_timeout {
74            break;
75        }
76
77        if let Ok((_, addr)) = socket.recv_from(&mut buf) {
78            let ip_addr = addr.ip();
79
80            if let IpAddr::V4(ip_addr) = ip_addr {
81                if let Ok(info) = get_speaker_info(ip_addr).await {
82                    if !discovered_speakers.contains(&info) {
83                        discovered_speakers.push(info);
84                    }
85                }
86            }
87        }
88    }
89
90    Ok(discovered_speakers)
91}