pub mod helpers {
use std::net::IpAddr;
use url::Url;
pub fn generate_target(address: IpAddr, token: &str) -> Result<Url, ()> {
let mut target = Url::parse("http://localhost").unwrap();
let path = format!("api/{}/", token);
target.set_path(&path[..]);
if target.set_ip_host(address).is_ok() {
return Ok(target);
}
Err(())
}
pub mod network {
use crate::lights::SendableState;
use url::Url;
pub enum AllowedMethod {
GET,
PUT,
POST,
}
impl std::convert::From<AllowedMethod> for reqwest::Method {
fn from(value: AllowedMethod) -> Self {
match value {
AllowedMethod::GET => reqwest::Method::GET,
AllowedMethod::POST => reqwest::Method::POST,
AllowedMethod::PUT => reqwest::Method::PUT,
}
}
}
pub type RequestTarget = (Url, AllowedMethod);
type ResponseResult = Result<reqwest::Response, reqwest::Error>;
type IndexedResponseResult = (usize, ResponseResult);
pub async fn send_request(
request_target: RequestTarget,
state: Option<&SendableState>,
client: &reqwest::Client,
) -> ResponseResult {
let (target, method) = request_target;
match method {
AllowedMethod::POST => client.post(target).json(&state).send().await,
AllowedMethod::GET => client.get(target).send().await,
AllowedMethod::PUT => client.put(target).json(&state).send().await,
}
}
pub async fn send_request_indexed(
index: usize,
request_target: RequestTarget,
state: Option<&SendableState>,
client: &reqwest::Client,
) -> IndexedResponseResult {
(index, send_request(request_target, state, client).await)
}
pub async fn send_requests(
request_targets: impl IntoIterator<Item = RequestTarget>,
states: impl IntoIterator<Item = Option<&SendableState>>,
client: &reqwest::Client,
) -> Vec<ResponseResult> {
use tokio::stream::StreamExt;
let mut f: futures::stream::FuturesUnordered<_> = request_targets
.into_iter()
.zip(states.into_iter())
.enumerate()
.map(|(i, (target, state))| send_request_indexed(i, target, state, client))
.collect();
let mut res = Vec::with_capacity(f.len());
while let Some(tup) = f.next().await {
res.push(tup);
}
res.sort_by_key(|tuple| tuple.0);
res.into_iter().map(|tup| tup.1).collect()
}
}
}
pub mod bridge {
use super::{
helpers::{network::*, *},
lights::*,
};
use std::collections::BTreeMap;
use std::net::IpAddr;
use tokio::runtime::Runtime;
use url::Url;
#[derive(Debug)]
pub struct Bridge {
pub target: Url,
ip: IpAddr,
token: String,
client: reqwest::Client,
runtime: Runtime,
}
impl Bridge {
pub fn new(ip: IpAddr, token: String) -> Result<Self, ()> {
let target = generate_target(ip, &token)?;
let client = reqwest::Client::new();
let runtime = tokio::runtime::Builder::new()
.basic_scheduler()
.enable_all()
.build()
.expect("Could not create tokio runtime during the creation of the Bridge");
Ok(Bridge {
target,
ip,
token,
client,
runtime,
})
}
pub fn scan(&mut self) -> BTreeMap<u8, Light> {
let endpoint = self.get_endpoint("./lights", AllowedMethod::GET);
let fut = send_request(endpoint, None, &self.client);
let lights: BTreeMap<u8, Light> = self
.runtime
.block_on(async { fut.await?.json().await })
.expect("Could not completely decode/send request");
lights
}
pub fn state_to(&mut self, id: u8, new_state: &SendableState) -> reqwest::Response {
let endpoint =
self.get_endpoint(&format!("./lights/{}/state", id)[..], AllowedMethod::PUT);
self.runtime
.block_on(send_request(endpoint, Some(new_state), &self.client))
.expect(&format!("Could not send state to light: {}", id)[..])
}
pub fn state_to_multiple<'a>(
&mut self,
ids: impl IntoIterator<Item = u8>,
new_states: impl IntoIterator<Item = &'a SendableState>,
) -> Result<Vec<reqwest::Response>, reqwest::Error> {
let endpoints: Vec<_> = ids
.into_iter()
.map(|id| {
self.get_endpoint(&format!("./lights/{}/state", id)[..], AllowedMethod::PUT)
})
.collect();
let states = new_states.into_iter().map(Some);
self.runtime
.block_on(send_requests(endpoints, states, &self.client))
.into_iter()
.collect()
}
fn get_endpoint(&self, s: &str, method: AllowedMethod) -> RequestTarget {
(self.target.join(s).unwrap(), method)
}
pub fn try_register(interactive: bool) -> Result<Self, String> {
use serde_json::Value;
let client = reqwest::Client::new();
let mut runtime = tokio::runtime::Builder::new()
.basic_scheduler()
.enable_all()
.build()
.expect("Could not create tokio runtime during registration");
let bridges = Self::find_bridges();
if interactive {
println!("Found the following bridges:\n{:#?}", bridges);
}
let body = serde_json::json!({ "devicetype": "lighthouse" });
let mut check = |ip: IpAddr| -> Value {
runtime.block_on(async {
client
.post(&format!("http://{}/api", ip))
.json(&body)
.send()
.await
.unwrap()
.json()
.await
.unwrap()
})
};
let bridge_ip;
let mut response;
if bridges.is_empty() {
return Err(String::from("Could not find any bridges on the network"));
} else {
if interactive {
println!(
"Will try to register. Please press the connection button on your bridge"
);
}
'wait_for_button: loop {
if interactive {
println!("Waiting for button press...");
}
std::thread::sleep(std::time::Duration::from_secs(3));
for ip in &bridges {
response = check(*ip);
if response[0]["error"]["type"] == 101 {
continue;
} else {
bridge_ip = ip;
break 'wait_for_button;
}
}
}
}
let token = response[0]["success"]["username"].to_string();
let target = generate_target(*bridge_ip, &token)
.expect("Could not create the required target after registration");
Ok(Bridge {
target,
ip: *bridge_ip,
token,
client,
runtime,
})
}
pub fn find_bridges() -> Vec<IpAddr> {
use ssdp::header::{HeaderMut, Man, MX, ST};
use ssdp::message::{Multicast, SearchRequest};
println!("Searching for bridges (5s)...");
let mut request = SearchRequest::new();
request.set(Man);
request.set(MX(5));
request.set(ST::Target(ssdp::FieldMap::URN(
"urn:schemas-upnp-org:device:Basic:1".into(),
)));
let devices = request
.multicast()
.expect("Could not perform multicast request");
let mut result: Vec<IpAddr> = devices.into_iter().map(|(_, src)| src.ip()).collect();
result.sort();
result.dedup();
result
}
pub fn system_info(&mut self) {
let fut = send_request(
self.get_endpoint("./lights", AllowedMethod::GET),
None,
&self.client,
);
match self
.runtime
.block_on(async { fut.await.expect("Could not perform request").json().await })
{
Ok(resp) => {
let r: serde_json::Value = resp;
println!("{}", serde_json::to_string_pretty(&r).unwrap());
}
Err(e) => {
println!("Could not send the get request: {}", e);
}
};
}
#[cfg(feature = "persist")]
pub fn from_env() -> Bridge {
let ip = std::env::var("HUE_BRIDGE_IP")
.expect("Could not find `HUE_BRIDGE_IP` environment variable.")
.parse()
.expect("Could not parse the address in the variable: HUE_BRIDGE_IP.");
let key = std::env::var("HUE_BRIDGE_KEY")
.expect("Could not find `HUE_BRIDGE_KEY` environment variable.");
Bridge::new(ip, key).expect(&format!("Could not create new bridge (IP {})", ip)[..])
}
#[cfg(feature = "persist")]
pub fn to_file(&self, filename: &str) -> std::io::Result<()> {
use std::io::prelude::*;
let mut file = std::fs::File::create(filename)?;
file.write_all(format!("{}\n{}", self.ip, self.token).as_ref())?;
Ok(())
}
#[cfg(feature = "persist")]
pub fn from_file(filename: &str) -> std::io::Result<Self> {
use std::io::{BufRead, BufReader};
let file = std::fs::File::open(filename)?;
let reader = BufReader::new(file);
let lines: Vec<String> = reader
.lines()
.enumerate()
.map(|(idx, line)| line.unwrap_or_else(|_| format!("Could not read line {}", idx)))
.collect();
assert!(lines.len() == 2);
Ok(Bridge::new(
lines[0].parse().expect("Could not parse the provided IP"),
lines[1].clone(),
)
.expect("Could not create Bridge"))
}
}
}
pub mod lights {
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct SendableState {
#[serde(skip_serializing_if = "Option::is_none")]
pub on: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bri: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hue: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sat: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub effect: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub xy: Option<[f32; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub alert: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transitiontime: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub colormode: Option<String>,
}
impl Default for SendableState {
fn default() -> Self {
Self {
on: None,
bri: None,
hue: None,
sat: None,
effect: None,
xy: None,
alert: None,
transitiontime: Some(1),
colormode: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct State {
pub on: bool,
pub bri: u8,
pub hue: u16,
pub sat: u8,
pub effect: String,
pub xy: [f32; 2],
pub ct: u32,
pub alert: String,
pub colormode: String,
pub mode: String,
pub reachable: bool,
}
impl From<State> for SendableState {
fn from(state: State) -> Self {
Self {
on: Some(state.on),
bri: Some(state.bri),
hue: Some(state.hue),
sat: Some(state.sat),
effect: Some(state.effect),
xy: Some(state.xy),
alert: None,
transitiontime: Some(1),
colormode: Some(state.colormode),
}
}
}
impl std::fmt::Display for Light {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{{ on: {} }} : {}", self.state.on, self.name)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Light {
pub state: State,
pub swupdate: Value,
pub r#type: String,
pub name: String,
pub modelid: String,
pub manufacturername: String,
pub productname: String,
pub capabilities: Value,
pub config: Value,
pub uniqueid: String,
pub swversion: String,
pub swconfigid: String,
pub productid: String,
}
#[macro_export]
macro_rules! state {
($($i:ident:$v:expr), *) => {
&$crate::lights::SendableState {
$($i: Some($v),) *
..$crate::lights::SendableState::default()
}
};
(nonref; $($i:ident:$v:expr),*) => {
$crate::lights::SendableState {
$($i: Some($v),)*
..$crate::lights::SendableState::default()
}
};
(from: $state:expr; $($i:ident:$v:expr),*) => {{
let mut sendable = $crate::lights::SendableState::from($state);
$(sendable.$i = Some($v);)*
sendable
}};
}
}
#[cfg(feature = "color")]
pub mod color {
use palette::{rgb::Srgb, Hsl};
pub fn rgb_to_xy(rgb: Vec<u8>) -> [f32; 2] {
let standardise = |c: u8| {
let val = (c as f32) / 255.0;
if val > 0.04045 {
((val + 0.055) / (1.0 + 0.055)).powf(2.4)
} else {
val / 12.92
}
};
let cnv: Vec<f32> = rgb.into_iter().map(standardise).collect();
let (red, green, blue) = (cnv[0], cnv[1], cnv[2]);
let x = red * 0.664_511 + green * 0.154_324 + blue * 0.162_028;
let y = red * 0.283_881 + green * 0.668_433 + blue * 0.047_685;
let z = red * 0.000_088 + green * 0.072_310 + blue * 0.986_039;
let denominator = x + y + z;
[x / denominator, y / denominator]
}
pub fn rgb_to_hsl(rgb: Vec<u8>) -> (u16, u8, u8) {
let standard: Vec<f32> = rgb
.into_iter()
.map(|val: u8| (val as f32) / 255.0)
.collect();
let (red, green, blue) = (standard[0], standard[1], standard[2]);
let hsl: Hsl = Srgb::new(red, green, blue).into();
let (h, s, l) = hsl.into_components();
(
(h.to_positive_degrees() / 360.0 * 65535.0) as u16,
(s * 254.0) as u8,
(l * 254.0) as u8,
)
}
pub fn hex_to_hsl(s: &str) -> Result<(u16, u8, u8), std::num::ParseIntError> {
let rgb = hex_to_rgb(s)?;
Ok(rgb_to_hsl(rgb))
}
pub fn hex_to_rgb(s: &str) -> Result<Vec<u8>, std::num::ParseIntError> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16))
.collect()
}
}
mod tests {
mod helpers {
#[test]
fn test_generate_target() {
use std::net::IpAddr;
use url::Url;
let addr: IpAddr = "192.168.0.1".parse().unwrap();
assert_eq!(
crate::helpers::generate_target(addr, "tokengoeshere1234").unwrap(),
Url::parse("http://192.168.0.1/api/tokengoeshere1234/").unwrap()
)
}
#[test]
fn test_httpbin_get() {
use crate::helpers::network::{send_requests, AllowedMethod};
let targets = vec![
(
url::Url::parse("http://httpbin.org/get").unwrap(),
AllowedMethod::GET,
),
(
url::Url::parse("http://httpbin.org/post").unwrap(),
AllowedMethod::POST,
),
(
url::Url::parse("http://httpbin.org/put").unwrap(),
AllowedMethod::PUT,
),
];
let states = vec![None, None, None];
let client = reqwest::Client::new();
let correct: bool = tokio::runtime::Runtime::new()
.unwrap()
.block_on(send_requests(targets, states, &client))
.into_iter()
.all(|r| r.unwrap().status() == 200);
assert!(correct);
}
#[test]
fn test_empty_state_macro() {
use crate::lights::*;
use crate::*;
let ref_state = state!();
let nonref_state = state!();
assert_eq!(ref_state, &SendableState::default());
assert_eq!(nonref_state, &SendableState::default());
}
#[test]
fn test_state_macro() {
use crate::lights::*;
use crate::*;
let ref_state = state!(on: true,
bri: 230,
hue: 100,
sat: 20,
effect: String::from("none"),
xy: [1.0, 1.0],
alert: String::from("none"),
colormode: String::from("xy"),
transitiontime: 2);
let nonref_state = state!(nonref;
on: true,
bri: 230,
hue: 100,
sat: 20,
effect: String::from("none"),
xy: [1.0, 1.0],
alert: String::from("none"),
colormode: String::from("xy"),
transitiontime: 2);
let truth = SendableState {
on: Some(true),
bri: Some(230),
hue: Some(100),
sat: Some(20),
effect: Some(String::from("none")),
xy: Some([1.0, 1.0]),
alert: Some(String::from("none")),
colormode: Some(String::from("xy")),
transitiontime: Some(2),
};
assert_eq!(ref_state, &truth);
assert_eq!(nonref_state, truth);
}
#[test]
fn test_from_state() {
use crate::lights::*;
use crate::*;
let mut s = State {
on: true,
bri: 100,
hue: 240,
sat: 20,
effect: String::from("none"),
xy: [2.0, 2.0],
ct: 200,
alert: String::from("select"),
colormode: String::from("somemode"),
mode: String::from("mode"),
reachable: true,
};
let state_default = state!(from: s.clone(););
let state_changed = state!(from: s.clone(); on: false);
assert_eq!(SendableState::from(s.clone()), state_default);
s.on = false;
assert_eq!(SendableState::from(s), state_changed);
}
#[test]
fn test_bridge_serialization() {
use crate::*;
let filename = "test_bridge";
let mut b =
bridge::Bridge::new("127.0.0.1".parse().unwrap(), "<SOME-KEY>".to_owned()).unwrap();
b.to_file(filename);
let b2 = bridge::Bridge::from_file(filename).unwrap();
assert!(b.target == b2.target);
}
}
}