Add config

This commit is contained in:
Daan Boerlage 2022-04-09 22:51:17 +02:00
parent a48b5c3f9c
commit 01a9671f2f
Signed by: daan
GPG key ID: FCE070E1E4956606
10 changed files with 193 additions and 20 deletions

73
Cargo.lock generated
View file

@ -114,6 +114,26 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 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]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.31" version = "0.8.31"
@ -271,6 +291,17 @@ dependencies = [
"slab", "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]] [[package]]
name = "h2" name = "h2"
version = "0.3.13" version = "0.3.13"
@ -351,6 +382,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
"dirs",
"eyre", "eyre",
"fern", "fern",
"futures", "futures",
@ -359,6 +391,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"toml",
] ]
[[package]] [[package]]
@ -742,6 +775,17 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.3" version = "0.5.3"
@ -1030,6 +1074,26 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" 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]] [[package]]
name = "time" name = "time"
version = "0.1.44" version = "0.1.44"
@ -1122,6 +1186,15 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "toml"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.1" version = "0.3.1"

View file

@ -16,3 +16,5 @@ reqwest = { version = "0.11", features = ["json", "rustls-tls-native-roots", "na
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
clap = { version = "3.1.8", features = ["derive"] } clap = { version = "3.1.8", features = ["derive"] }
toml = "0.5"
dirs = "4.0"

View file

@ -3,11 +3,12 @@ use serde::{Deserialize, Serialize};
use crate::api::models::lights::{Light, On}; use crate::api::models::lights::{Light, On};
use crate::api::models::Response; use crate::api::models::Response;
use crate::config::Config;
use crate::{api, ToggleState}; use crate::{api, ToggleState};
pub async fn all() -> Result<Vec<Light>> { pub async fn all(config: Config) -> Result<Vec<Light>> {
let request_url = format!("https://{}/clip/v2/resource/light", api::HUB_IP); let request_url = format!("https://{}/clip/v2/resource/light", config.hostname);
let client = api::get_client()?; let client = api::get_client(&config)?;
match client.get(request_url).send().await { match client.get(request_url).send().await {
Ok(response) => Ok(response.json::<Response<Light>>().await?.data), Ok(response) => Ok(response.json::<Response<Light>>().await?.data),
@ -29,16 +30,16 @@ pub struct LightStatePut {
on: On, on: On,
} }
pub async fn toggle(id: String, state: ToggleState) -> Result<()> { pub async fn toggle(config: Config, id: String, state: ToggleState) -> Result<()> {
let request_url = format!("https://{}/clip/v2/resource/light/{}", api::HUB_IP, id); let request_url = format!("https://{}/clip/v2/resource/light/{}", config.hostname, id);
let client = api::get_client()?; let client = api::get_client(&config)?;
let target_state: LightStatePut = LightStatePut { let target_state: LightStatePut = LightStatePut {
on: On { on: On {
on: match state { on: match state {
ToggleState::On => true, ToggleState::On => true,
ToggleState::Off => false, 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, Some(light) => !light.on.on,
None => true, None => true,
}, },

View file

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

View file

@ -1,12 +1,14 @@
use eyre::Result; use eyre::Result;
use crate::api; use crate::api;
use crate::config::Config;
pub async fn exec() -> Result<()> { pub async fn exec(config: Config) -> Result<()> {
let lights = api::lights::all().await?; let lights = api::lights::all(config).await?;
for light in lights { 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(()) Ok(())

View file

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

33
src/config/config_file.rs Normal file
View file

@ -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<String>,
pub host: Option<Host>,
}
#[derive(Deserialize)]
pub struct Host {
pub hostname: String,
}
pub async fn read() -> Result<Option<ConfigFile>> {
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))
}

View file

@ -0,0 +1 @@

59
src/config/mod.rs Normal file
View file

@ -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<Config> {
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(())
}

View file

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