Initial Commit

This commit is contained in:
Daan Boerlage 2022-04-09 18:05:14 +02:00
commit a48b5c3f9c
Signed by: daan
GPG key ID: FCE070E1E4956606
13 changed files with 1830 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/lights

1409
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "hue-control"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
eyre = "0.6"
tokio = { version = "1", features = ["full"] }
futures = "0.3.21"
log = "0.4"
fern = "0.6"
chrono = "0.4"
reqwest = { version = "0.11", features = ["json", "rustls-tls-native-roots", "native-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
clap = { version = "3.1.8", features = ["derive"] }

4
readme.md Normal file
View file

@ -0,0 +1,4 @@
# Hue Control
API Docs: https://developers.meethue.com/develop/hue-api-v2/
API Reference: https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_light_get

71
src/api/lights.rs Normal file
View file

@ -0,0 +1,71 @@
use eyre::{eyre, Result};
use serde::{Deserialize, Serialize};
use crate::api::models::lights::{Light, On};
use crate::api::models::Response;
use crate::{api, ToggleState};
pub async fn all() -> Result<Vec<Light>> {
let request_url = format!("https://{}/clip/v2/resource/light", api::HUB_IP);
let client = api::get_client()?;
match client.get(request_url).send().await {
Ok(response) => Ok(response.json::<Response<Light>>().await?.data),
Err(e) => {
if let Some(status_code) = e.status() {
Err(eyre!(
"Failed to get all lights, status code was {:?}",
status_code
))
} else {
Err(eyre!("Failed to get all lights"))
}
}
}
}
#[derive(Serialize, Deserialize)]
pub struct LightStatePut {
on: On,
}
pub async fn toggle(id: String, state: ToggleState) -> Result<()> {
let request_url = format!("https://{}/clip/v2/resource/light/{}", api::HUB_IP, id);
let client = api::get_client()?;
let target_state: LightStatePut = LightStatePut {
on: On {
on: match state {
ToggleState::On => true,
ToggleState::Off => false,
ToggleState::Swap => match all().await?.into_iter().find(|x| x.id == id) {
Some(light) => !light.on.on,
None => true,
},
},
},
};
client.put(request_url).json(&target_state).send().await?;
Ok(())
}
// pub async fn get_light() -> Result<Light> {
// let request_url = format!("https://{}/clip/v2/resource/light/{}", api::HUB_IP, id);
// let client = api::get_client().unwrap();
//
// match client.get(request_url).send().await {
// Ok(response) => {
// let deserialized = response.json::<Response<Light>>().await?;
// Ok(deserialized.data.first()?)
// },
// Err(e) => {
// if let Some(status_code) = e.status(){
// Err(eyre!("Failed to get all lights, status code was {:?}", status_code))
// } else {
// Err(eyre!("Failed to get all lights"))
// }
// }
// }
// }

24
src/api/mod.rs Normal file
View file

@ -0,0 +1,24 @@
pub mod lights;
pub mod models;
use eyre::Result;
use reqwest::{header, Client};
const HUB_IP: &'static str = "10.60.0.163";
const HUE_TOKEN: &'static str = "sNLQz7sOVfji2VA42jeLwhx2rnwBCkYu-OVc2ddw";
pub fn get_client() -> Result<Client> {
let mut headers = header::HeaderMap::new();
headers.insert(
"hue-application-key",
header::HeaderValue::from_static(HUE_TOKEN),
);
let client = reqwest::Client::builder()
.use_native_tls()
.danger_accept_invalid_certs(true)
.default_headers(headers)
.build()?;
Ok(client)
}

191
src/api/models/lights.rs Normal file
View file

@ -0,0 +1,191 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Light {
// #[serde(rename = "alert")]
// pub alert: Alert,
// #[serde(rename = "color")]
// pub color: Option<Color>,
// #[serde(rename = "color_temperature")]
// pub color_temperature: ColorTemperature,
// #[serde(rename = "color_temperature_delta")]
// pub color_temperature_delta: Delta,
// #[serde(rename = "dimming")]
// pub dimming: Dimming,
// #[serde(rename = "dimming_delta")]
// pub dimming_delta: Delta,
// #[serde(rename = "dynamics")]
// pub dynamics: Dynamics,
#[serde(rename = "id")]
pub id: String,
#[serde(rename = "id_v1")]
pub id_v1: String,
#[serde(rename = "metadata")]
pub metadata: Metadata,
#[serde(rename = "mode")]
pub mode: String,
#[serde(rename = "on")]
pub on: On,
#[serde(rename = "owner")]
pub owner: Owner,
// #[serde(rename = "type")]
// pub datum_type: String,
//
// #[serde(rename = "effects")]
// pub effects: Option<Effects>,
}
#[derive(Serialize, Deserialize)]
pub struct Alert {
#[serde(rename = "action_values")]
pub action_values: Vec<String>,
}
#[derive(Serialize, Deserialize)]
pub struct Color {
#[serde(rename = "gamut")]
pub gamut: Option<Gamut>,
#[serde(rename = "gamut_type")]
pub gamut_type: Option<String>,
#[serde(rename = "xy")]
pub xy: Xy,
}
#[derive(Serialize, Deserialize)]
pub struct Gamut {
#[serde(rename = "blue")]
pub blue: Xy,
#[serde(rename = "green")]
pub green: Xy,
#[serde(rename = "red")]
pub red: Xy,
}
#[derive(Serialize, Deserialize)]
pub struct Xy {
#[serde(rename = "x")]
pub x: f64,
#[serde(rename = "y")]
pub y: f64,
}
#[derive(Serialize, Deserialize)]
pub struct ColorTemperature {
#[serde(rename = "mirek")]
pub mirek: i64,
#[serde(rename = "mirek_schema")]
pub mirek_schema: MirekSchema,
#[serde(rename = "mirek_valid")]
pub mirek_valid: bool,
}
#[derive(Serialize, Deserialize)]
pub struct MirekSchema {
#[serde(rename = "mirek_maximum")]
pub mirek_maximum: i64,
#[serde(rename = "mirek_minimum")]
pub mirek_minimum: i64,
}
#[derive(Serialize, Deserialize)]
pub struct Delta {}
#[derive(Serialize, Deserialize)]
pub struct Dimming {
#[serde(rename = "brightness")]
pub brightness: f64,
#[serde(rename = "min_dim_level")]
pub min_dim_level: f64,
}
#[derive(Serialize, Deserialize)]
pub struct Dynamics {
#[serde(rename = "speed")]
pub speed: i64,
#[serde(rename = "speed_valid")]
pub speed_valid: bool,
#[serde(rename = "status")]
pub status: DynamicsStatus,
#[serde(rename = "status_values")]
pub status_values: Vec<DynamicsStatus>,
}
#[derive(Serialize, Deserialize)]
pub struct Effects {
#[serde(rename = "effect_values")]
pub effect_values: Vec<StatusElement>,
#[serde(rename = "status")]
pub status: StatusElement,
#[serde(rename = "status_values")]
pub status_values: Vec<StatusElement>,
}
#[derive(Serialize, Deserialize)]
pub struct Metadata {
#[serde(rename = "archetype")]
pub archetype: String,
#[serde(rename = "name")]
pub name: String,
}
#[derive(Serialize, Deserialize)]
pub struct On {
#[serde(rename = "on")]
pub on: bool,
}
#[derive(Serialize, Deserialize)]
pub struct Owner {
#[serde(rename = "rid")]
pub rid: String,
#[serde(rename = "rtype")]
pub rtype: String,
}
#[derive(Serialize, Deserialize)]
pub enum DynamicsStatus {
#[serde(rename = "dynamic_palette")]
DynamicPalette,
#[serde(rename = "none")]
None,
}
#[derive(Serialize, Deserialize)]
pub enum StatusElement {
#[serde(rename = "candle")]
Candle,
#[serde(rename = "fire")]
Fire,
#[serde(rename = "no_effect")]
NoEffect,
}

12
src/api/models/mod.rs Normal file
View file

@ -0,0 +1,12 @@
pub mod lights;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Response<T> {
#[serde(rename = "errors")]
pub errors: Vec<Option<serde_json::Value>>,
#[serde(rename = "data")]
pub data: Vec<T>,
}

42
src/args.rs Normal file
View file

@ -0,0 +1,42 @@
use clap::{Parser, Subcommand};
use std::str::FromStr;
#[derive(Parser)]
#[clap(author, version, about, long_about = None)]
#[clap(propagate_version = true)]
pub struct Args {
#[clap(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// List all lights
List,
/// Inspect a light
Inspect,
/// Toggle a light
Toggle { id: String, state: Option<String> },
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ToggleState {
On,
Off,
Swap,
}
impl FromStr for ToggleState {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let res = match s {
"on" => ToggleState::On,
"off" => ToggleState::Off,
"swap" => ToggleState::Swap,
_ => ToggleState::Swap,
};
Ok(res)
}
}

13
src/commands/list.rs Normal file
View file

@ -0,0 +1,13 @@
use eyre::Result;
use crate::api;
pub async fn exec() -> Result<()> {
let lights = api::lights::all().await?;
for light in lights {
println!("{} - {}", light.id, light.metadata.name)
}
Ok(())
}

2
src/commands/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod list;
pub mod toggle;

16
src/commands/toggle.rs Normal file
View file

@ -0,0 +1,16 @@
use eyre::Result;
use std::str::FromStr;
use crate::{api, ToggleState};
pub async fn exec(id: &String, state: &Option<String>) -> Result<()> {
let target_state = if let Some(target) = &state {
ToggleState::from_str(target.as_str()).unwrap()
} else {
ToggleState::Swap
};
api::lights::toggle(id.clone(), target_state).await?;
Ok(())
}

26
src/main.rs Normal file
View file

@ -0,0 +1,26 @@
mod api;
mod args;
mod commands;
use crate::args::ToggleState;
use clap::Parser;
use eyre::Result;
#[tokio::main]
async fn main() -> Result<()> {
let args = args::Args::parse();
match &args.command {
args::Commands::Toggle { id, state } => {
commands::toggle::exec(id, state).await?;
}
args::Commands::List => {
commands::list::exec().await?;
}
args::Commands::Inspect => {
panic!("Not implemented");
}
}
Ok(())
}