Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: kali/hue.rs
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: mazhewitt/hue.rs
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.
  • 9 commits
  • 4 files changed
  • 1 contributor

Commits on Apr 6, 2023

  1. Merge pull request #1 from kali/main

    upstream
    mazhewitt authored Apr 6, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    1ea218d View commit details

Commits on Apr 8, 2023

  1. Copy the full SHA
    cdeea11 View commit details

Commits on Apr 9, 2023

  1. Copy the full SHA
    703e5c7 View commit details
  2. merge upstream

    mazhewitt committed Apr 9, 2023
    Copy the full SHA
    87009c2 View commit details

Commits on Apr 10, 2023

  1. testes mdns use case

    mazhewitt committed Apr 10, 2023
    Copy the full SHA
    ab17baf View commit details
  2. added a mock for http get

    mazhewitt committed Apr 10, 2023
    Copy the full SHA
    cb4e7bf View commit details

Commits on Apr 14, 2023

  1. small refactor

    mazhewitt committed Apr 14, 2023
    Copy the full SHA
    c8d8dcc View commit details
  2. Copy the full SHA
    42c918a View commit details
  3. Copy the full SHA
    f7a771b View commit details
Showing with 118 additions and 66 deletions.
  1. +5 −1 Cargo.toml
  2. +94 −65 src/disco.rs
  3. +18 −0 src/hue_http.rs
  4. +1 −0 src/lib.rs
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -14,11 +14,15 @@ edition = "2018"
[dependencies]
thiserror = "1.0.20"
regex = "1.3"
reqwest = { version = "0.10", features = [ "blocking", "json", "rustls-tls" ], default-features = false}
reqwest = { version = "0.11.16", features = [ "blocking", "json", "rustls-tls" ], default-features = false}
serde = { version = "1", features = ["derive"]}
serde_json = "1"
ssdp-probe = "0.2"
futures-util = "0.3.17"
futures = "0.3.17"
mdns = "3.0.0"
async-std = "1.12.0"
dns-parser = "0.8.0"
mockall = "0.11.4"
mockall_double = "0.3.0"

159 changes: 94 additions & 65 deletions src/disco.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
use crate::{HueError, HueError::DiscoveryError};
use crate::{HueError, HueError::DiscoveryError, hue_http::*};
use serde_json::{Map, Value};
use futures_util::{pin_mut, stream::StreamExt};
use futures::executor::block_on;
use mdns::{Record, RecordKind};
use mdns::{Error, Record};
use std::{net::IpAddr, time::Duration};
use async_std::future;
use async_std::prelude::Stream;

#[mockall_double::double]
use hue_http::get_request;


// As Per instrucitons at
// https://developers.meethue.com/develop/application-design-guidance/hue-bridge-discovery/
pub fn discover_hue_bridge() -> Result<IpAddr, HueError> {

let bridge_ftr = discover_hue_bridge_m_dns();
let bridge = block_on(bridge_ftr);
let bridge = discover_hue_bridge_m_dns();
match bridge{
Ok(bridge_ip) => Ok(bridge_ip),
Err(e) => {
Err(_e) => {
let n_upnp_result = discover_hue_bridge_n_upnp();
if n_upnp_result.is_err() {
Err(DiscoveryError {
@@ -27,9 +29,12 @@ pub fn discover_hue_bridge() -> Result<IpAddr, HueError> {
}
}


const MEET_HUE_URL : &str= "https://discovery.meethue.com";

pub fn discover_hue_bridge_n_upnp() -> Result<IpAddr, HueError> {
let objects: Vec<Map<String, Value>> =
reqwest::blocking::get("https://discovery.meethue.com/")?.json()?;
let response = get_request::get(MEET_HUE_URL);
let objects: Vec<Map<String, Value>> = serde_json::from_str(response?.as_str())?;

if objects.len() == 0 {
Err(DiscoveryError {
@@ -49,74 +54,34 @@ pub fn discover_hue_bridge_n_upnp() -> Result<IpAddr, HueError> {
.parse()?)
}

pub fn discover_hue_bridge_upnp() -> Result<IpAddr, HueError> {
// use 'IpBridge' as a marker and a max duration of 5s as per
// https://developers.meethue.com/develop/application-design-guidance/hue-bridge-discovery/
// this method is now deprecated
Ok(
ssdp_probe::ssdp_probe_v4(br"IpBridge", 1, std::time::Duration::from_secs(5))?
.first()
.map(|it| it.to_owned().into())
.ok_or(DiscoveryError {
msg: "could not find bridge with ssdp_probe".into(),
})?,
)
}

// Define the service name for hue bridge
const SERVICE_NAME: &str = "_hue._tcp.local";

// Define a function that discovers a hue bridge using mDNS
pub async fn discover_hue_bridge_m_dns() -> Result<IpAddr, HueError> {
// Iterate through responses from each hue bridge device, asking for new devices every 15s
let stream_disc = mdns::discover::all(SERVICE_NAME, Duration::from_secs(1));
let stream = match stream_disc {
Ok(s) => s.listen(),
Err(_e) => {
return Err(DiscoveryError {
msg: _e.to_string(),
})
}
};
pin_mut!(stream);
let response = async_std::future::timeout(Duration::from_secs(5), stream.next()).await;
match response {
Ok(Some(Ok(response))) => {
// Get the first IP address from the response
let ip = response
.records()
.filter_map(to_ip_addr)
.next()
.ok_or(DiscoveryError {
msg: "No IP address found in response".into(),
})?;
Ok(ip)
}
Ok(Some(Err(e))) => Err(DiscoveryError {
msg: e.to_string(),
}),
Ok(None) => Err(DiscoveryError {
msg: "No response from bridge".into(),
}),
Err(_e) => Err(DiscoveryError {
msg: "No response from bridge".into(),
}),
}
pub fn discover_hue_bridge_m_dns() -> Result<IpAddr, HueError> {
read_mdns_response(mdns::discover::all(SERVICE_NAME, Duration::from_secs(1)).unwrap().listen())
}

// Define a helper function that converts a record to an IP address
fn to_ip_addr(record: &Record) -> Option<IpAddr> {
match record.kind {
RecordKind::A(addr) => Some(addr.into()),
RecordKind::AAAA(addr) => Some(addr.into()),
_ => None,
}
fn read_mdns_response(stream: impl Stream<Item=Result<mdns::Response, Error>> + Sized) -> Result<IpAddr, HueError> {
pin_mut!(stream);
let response_or = block_on(async_std::future::timeout(Duration::from_secs(5), stream.next()));
let response = match response_or {
Ok(Some(Ok(response))) => response,
Ok(Some(Err(e))) => Err(DiscoveryError { msg: format!("Error reading mDNS response: {}", e) })?,
Ok(None) => Err(DiscoveryError { msg: "No mDNS response found".into() })?,
Err(_) => Err(DiscoveryError { msg: "Timed out waiting for mDNS response".into() })?,
};
response.ip_addr().ok_or(DiscoveryError { msg: "No IP address found".into() })
}


#[cfg(test)]
mod tests {
use mdns::RecordKind::A;
use futures::FutureExt;
use super::*;


#[test]
#[ignore]
fn test_discover_hue_bridge() {
@@ -125,4 +90,68 @@ mod tests {
let ip = ip.unwrap();
assert_eq!(ip.to_string(), "192.168.1.149");
}

// test resolve_mdns_result using mock response
#[test]
fn test_read_mdns_response() {

let record = Record {
name: "_hue._tcp.local".to_string(),
class: dns_parser::Class::IN,
ttl: 0,
kind: (A("192.168.1.145".parse().unwrap())),
};

let response = mdns::Response {
answers: vec![record],
nameservers: vec![],
additional: vec![],
};

let stream = futures::stream::iter(vec![Ok::<mdns::Response, Error>(response)]);
let ip = read_mdns_response(stream).unwrap();
assert_eq!(ip.to_string(), "192.168.1.145");
}

#[test]
fn should_error_when_no_mdns_bridge_found() {
let stream = futures::stream::iter(vec![]);
let ip = read_mdns_response(stream);
assert!(ip.is_err());
}

#[test]
fn should_timeout_when_timeout_exceeded() {
// this stream never returns a value
let stream = futures::future::pending::<Result<mdns::Response, Error>>().into_stream();
let ip = read_mdns_response(stream);
//assert that the error message is "Timed out waiting for mDNS response"
assert!(ip.is_err());
assert_eq!(ip.err().unwrap().to_string(), "A discovery error occurred: Timed out waiting for mDNS response");
}

// a test for the n-upnp discovery method
#[test]
#[ignore]
fn test_discover_hue_bridge_n_upnp() {
let ip = discover_hue_bridge_n_upnp();
assert!(ip.is_ok());
let ip = ip.unwrap();
assert_eq!(ip.to_string(), "192.168.1.149");
}

const TEST_HUE_RESPONSE : &str = "[{\"id\":\"ecb5fafffe8381f2\",\"internalipaddress\":\"192.168.1.143\",\"port\":443}]";
// a test for the n-upnp discovery method using a mock get request
#[test]
fn test_discover_hue_bridge_n_upnp_mock() {
let mock = get_request::get_context();
mock.expect()
.returning(|_| Ok(TEST_HUE_RESPONSE.to_string()));
let ip = discover_hue_bridge_n_upnp();
assert!(ip.is_ok());
let ip = ip.unwrap();
assert_eq!(ip.to_string(), "192.168.1.143")
}


}
18 changes: 18 additions & 0 deletions src/hue_http.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

// A wrapper for reqwest::blocking::get that can be mocked.
pub mod hue_http {
use mockall::automock;
#[automock]
pub mod get_request {
use crate::HueError;

pub fn get(url: &str) -> Result<String, HueError> {
let response = reqwest::blocking::get(url.to_string());
match response {
Ok(response) => Ok(response.text()?),
Err(e) => Err(e.into())
}
}

}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -78,6 +78,7 @@ pub type Result<T> = std::result::Result<T, HueError>;
mod bridge;
mod command_parser;
mod disco;
mod hue_http;

pub use bridge::*;
pub use command_parser::*;