From 01a9671f2f6ce87561b5127965eb758307bc13f3 Mon Sep 17 00:00:00 2001 From: Daan Boerlage Date: Sat, 9 Apr 2022 22:51:17 +0200 Subject: [PATCH] Add config --- Cargo.lock | 73 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 ++- src/api/lights.rs | 15 ++++---- src/api/mod.rs | 9 +++-- src/commands/list.rs | 8 +++-- src/commands/toggle.rs | 5 +-- src/config/config_file.rs | 33 ++++++++++++++++++ src/config/environment.rs | 1 + src/config/mod.rs | 59 +++++++++++++++++++++++++++++++ src/main.rs | 6 ++-- 10 files changed, 193 insertions(+), 20 deletions(-) create mode 100644 src/config/config_file.rs create mode 100644 src/config/environment.rs create mode 100644 src/config/mod.rs diff --git a/Cargo.lock b/Cargo.lock index fa3afbb..5145ff7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,6 +114,26 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "encoding_rs" version = "0.8.31" @@ -271,6 +291,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + [[package]] name = "h2" version = "0.3.13" @@ -351,6 +382,7 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "dirs", "eyre", "fern", "futures", @@ -359,6 +391,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "toml", ] [[package]] @@ -742,6 +775,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -1030,6 +1074,26 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.1.44" @@ -1122,6 +1186,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + [[package]] name = "tower-service" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 59c78f7..bb7a1e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,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"] } \ No newline at end of file +clap = { version = "3.1.8", features = ["derive"] } +toml = "0.5" +dirs = "4.0" \ No newline at end of file diff --git a/src/api/lights.rs b/src/api/lights.rs index 68b5c6d..afe5907 100644 --- a/src/api/lights.rs +++ b/src/api/lights.rs @@ -3,11 +3,12 @@ use serde::{Deserialize, Serialize}; use crate::api::models::lights::{Light, On}; use crate::api::models::Response; +use crate::config::Config; use crate::{api, ToggleState}; -pub async fn all() -> Result> { - let request_url = format!("https://{}/clip/v2/resource/light", api::HUB_IP); - let client = api::get_client()?; +pub async fn all(config: Config) -> Result> { + let request_url = format!("https://{}/clip/v2/resource/light", config.hostname); + let client = api::get_client(&config)?; match client.get(request_url).send().await { Ok(response) => Ok(response.json::>().await?.data), @@ -29,16 +30,16 @@ 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()?; +pub async fn toggle(config: Config, id: String, state: ToggleState) -> Result<()> { + let request_url = format!("https://{}/clip/v2/resource/light/{}", config.hostname, id); + let client = api::get_client(&config)?; 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) { + ToggleState::Swap => match all(config).await?.into_iter().find(|x| x.id == id) { Some(light) => !light.on.on, None => true, }, diff --git a/src/api/mod.rs b/src/api/mod.rs index 0659ed6..6af0a2f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,23 +1,22 @@ pub mod lights; pub mod models; +use crate::config::Config; 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 { +pub fn get_client(config: &Config) -> Result { let mut headers = header::HeaderMap::new(); headers.insert( "hue-application-key", - header::HeaderValue::from_static(HUE_TOKEN), + header::HeaderValue::from_str(config.token.as_str())?, ); let client = reqwest::Client::builder() .use_native_tls() .danger_accept_invalid_certs(true) .default_headers(headers) + .timeout(Duration::from_secs(3)) .build()?; Ok(client) diff --git a/src/commands/list.rs b/src/commands/list.rs index a766ee9..b6b8605 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -1,12 +1,14 @@ use eyre::Result; use crate::api; +use crate::config::Config; -pub async fn exec() -> Result<()> { - let lights = api::lights::all().await?; +pub async fn exec(config: Config) -> Result<()> { + let lights = api::lights::all(config).await?; for light in lights { - println!("{} - {}", light.id, light.metadata.name) + let icon = if light.on.on { "💡" } else { "🔌" }; + println!("{} - {} - {}", icon, light.id, light.metadata.name) } Ok(()) diff --git a/src/commands/toggle.rs b/src/commands/toggle.rs index 5e3d3d0..0e8b872 100644 --- a/src/commands/toggle.rs +++ b/src/commands/toggle.rs @@ -1,16 +1,17 @@ use eyre::Result; use std::str::FromStr; +use crate::config::Config; use crate::{api, ToggleState}; -pub async fn exec(id: &String, state: &Option) -> Result<()> { +pub async fn exec(config: Config, id: &String, state: &Option) -> 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?; + api::lights::toggle(config, id.clone(), target_state).await?; Ok(()) } diff --git a/src/config/config_file.rs b/src/config/config_file.rs new file mode 100644 index 0000000..a223f5a --- /dev/null +++ b/src/config/config_file.rs @@ -0,0 +1,33 @@ +use eyre::Result; +use serde::Deserialize; +use std::fs; +use std::path::Path; + +#[derive(Deserialize)] +pub struct ConfigFile { + pub token: Option, + pub host: Option, +} + +#[derive(Deserialize)] +pub struct Host { + pub hostname: String, +} + +pub async fn read() -> Result> { + let home_dir = match dirs::home_dir() { + Some(h) => h, + None => return Ok(None), + }; + + let config_file_path = home_dir.join(Path::new(".hue-control.toml")); + + let content = match fs::read_to_string(config_file_path) { + Ok(c) => c, + Err(_) => return Ok(None), + }; + + let config: ConfigFile = toml::from_str(&content)?; + + Ok(Some(config)) +} diff --git a/src/config/environment.rs b/src/config/environment.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/config/environment.rs @@ -0,0 +1 @@ + diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..cc39c8d --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,59 @@ +mod config_file; +mod environment; + +use eyre::{eyre, Result}; + +#[derive(Debug)] +pub struct Config { + pub hostname: String, + pub token: String, +} + +pub async fn resolve() -> Result { + let mut config = Config { + hostname: "".to_string(), + token: "".to_string(), + }; + + let config_file = config_file::read().await?; + if let Some(c) = config_file { + if let Some(token) = c.token { + config.token = token; + } + if let Some(host) = c.host { + config.hostname = host.hostname + } + } + + if let Ok(token) = std::env::var("HUE_TOKEN") { + config.token = token; + } + + if let Ok(host) = std::env::var("HUE_HOSTNAME") { + config.hostname = host; + } + + println!("{:?}", config); + + if let Err(e) = validate_config(&config) { + return Err(e); + } + + Ok(config) +} + +fn validate_config(config: &Config) -> Result<()> { + if config.token.is_empty() { + return Err(eyre!("Token config variable is empty")); + } + + if config.token.len() != 40 || !config.token.is_ascii() { + return Err(eyre!("Token format is invalid")); + } + + if config.hostname.is_empty() { + return Err(eyre!("hostname is empty")); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 73a1c71..cad87b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod api; mod args; mod commands; +mod config; use crate::args::ToggleState; use clap::Parser; @@ -9,13 +10,14 @@ use eyre::Result; #[tokio::main] async fn main() -> Result<()> { let args = args::Args::parse(); + let config = config::resolve().await?; match &args.command { args::Commands::Toggle { id, state } => { - commands::toggle::exec(id, state).await?; + commands::toggle::exec(config, id, state).await?; } args::Commands::List => { - commands::list::exec().await?; + commands::list::exec(config).await?; } args::Commands::Inspect => { panic!("Not implemented");