Initial Commit
This commit is contained in:
commit
a48b5c3f9c
13 changed files with 1830 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/lights
|
1409
Cargo.lock
generated
Normal file
1409
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal 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
4
readme.md
Normal 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
71
src/api/lights.rs
Normal 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
24
src/api/mod.rs
Normal 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
191
src/api/models/lights.rs
Normal 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
12
src/api/models/mod.rs
Normal 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
42
src/args.rs
Normal 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
13
src/commands/list.rs
Normal 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
2
src/commands/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod list;
|
||||||
|
pub mod toggle;
|
16
src/commands/toggle.rs
Normal file
16
src/commands/toggle.rs
Normal 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
26
src/main.rs
Normal 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(())
|
||||||
|
}
|
Loading…
Reference in a new issue