diff --git a/Cargo.lock b/Cargo.lock index 47b9e71..d2d446d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + [[package]] name = "bitflags" version = "1.3.2" @@ -214,6 +220,16 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -264,6 +280,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "envious" version = "0.2.2" @@ -304,6 +329,12 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + [[package]] name = "futures-task" version = "0.3.29" @@ -379,7 +410,9 @@ version = "0.1.0" dependencies = [ "axum", "envious", + "reqwest", "serde", + "serde_json", "tera", "thiserror", "tokio", @@ -388,6 +421,31 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "http" version = "0.2.9" @@ -447,6 +505,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "httparse", @@ -460,6 +519,20 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -483,6 +556,16 @@ dependencies = [ "cc", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "ignore" version = "0.4.20" @@ -500,6 +583,22 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itoa" version = "1.0.9" @@ -854,12 +953,97 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustls" +version = "0.21.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -881,6 +1065,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "serde" version = "1.0.190" @@ -960,6 +1154,15 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "slug" version = "0.1.4" @@ -995,6 +1198,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "syn" version = "2.0.38" @@ -1012,6 +1221,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tera" version = "1.19.1" @@ -1064,6 +1294,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.33.0" @@ -1071,6 +1316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", + "bytes", "libc", "mio", "pin-project-lite", @@ -1090,6 +1336,30 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -1267,12 +1537,44 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1335,6 +1637,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.88" @@ -1364,6 +1678,22 @@ version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + [[package]] name = "winapi" version = "0.3.9" @@ -1469,3 +1799,13 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] diff --git a/Cargo.toml b/Cargo.toml index 97cdb06..d749924 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,9 @@ edition = "2021" [dependencies] axum = "0.6.20" envious = "0.2.2" +reqwest = { version = "0.11.22", features = ["json", "rustls-tls"], default-features = false } serde = { version = "1.0.190", features = ["derive"] } +serde_json = "1.0.108" tera = "1.19.1" thiserror = "1.0.50" tokio = { version = "1.33.0", features = ["rt", "macros"] } diff --git a/src/main.rs b/src/main.rs index 7b7ed75..8ec2d9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ -use std::{net::SocketAddr, sync::Arc}; +use std::{collections::HashMap, net::SocketAddr, sync::Arc}; use axum::{ + extract::Path, http::StatusCode, - response::{Html, IntoResponse}, + response::{Html, IntoResponse, Redirect}, routing::get, Router, }; -use serde::{Deserialize, Serialize}; +use reqwest::Method; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tera::{Context, Tera}; use tower_http::trace::TraceLayer; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -15,6 +17,8 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; enum Error { #[error("Could not render tera template")] Template(#[from] tera::Error), + #[error("Failed to send HTTP request")] + Request(#[from] reqwest::Error), } impl IntoResponse for Error { @@ -22,7 +26,7 @@ impl IntoResponse for Error { tracing::error!("Failure in route: {self:?}"); match self { - Error::Template(_) => ( + Error::Template(_) | Error::Request(_) => ( StatusCode::INTERNAL_SERVER_ERROR, "an internal error occured", ) @@ -38,16 +42,130 @@ const TEMPLATE_DIR: &str = match option_env!("TEMPLATE_DIR") { struct AppState { templates: Tera, + token: String, + ha_url: String, + client: reqwest::Client, } type State = axum::extract::State>; +#[derive(Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum Status { + On, + Off, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[serde(rename_all = "snake_case")] +enum Component { + Salon, +} + +impl Component { + fn light_id(&self) -> &'static str { + match self { + Component::Salon => "switch.interrupteur_salon", + } + } +} + async fn index(state: State) -> Result, Error> { - let rendered = state.templates.render("index.html", &Context::new())?; + let mut status = HashMap::new(); + status.insert("salon", state.light_state(Component::Salon).await?); + + let mut ctx = Context::new(); + ctx.insert("status", &status); + + let rendered = state.templates.render("index.html", &ctx)?; Ok(rendered.into()) } +impl AppState { + async fn ha_request(&self, path: &str, data: &D, method: Method) -> Result + where + D: Serialize, + R: DeserializeOwned, + { + let url = format!("{}/api/{}", self.ha_url, path); + tracing::debug!("Performing request to '{url}'"); + + Ok(self + .client + .request(method, url) + .header("Authorization", &format!("Bearer {}", self.token)) + .json(data) + .send() + .await? + .json() + .await?) + } + + async fn light_state(&self, component: Component) -> Result { + #[derive(Deserialize)] + struct Rsp { + state: Status, + } + + Ok(self + .ha_request::<_, Rsp>( + &format!("states/{}", component.light_id()), + &(), + Method::GET, + ) + .await? + .state) + } +} + +#[derive(Deserialize)] +struct Empty {} + +async fn on(state: State, component: Path) -> Result { + let id = component.light_id(); + state + .ha_request::<_, Vec>( + "services/switch/turn_on", + &serde_json::json!({ + "entity_id": id, + }), + Method::POST, + ) + .await?; + + loop { + let status = state.light_state(*component).await?; + if status == Status::On { + break; + } + } + + Ok(Redirect::to("/")) +} + +async fn off(state: State, component: Path) -> Result { + let id = component.light_id(); + state + .ha_request::<_, Vec>( + "services/switch/turn_off", + &serde_json::json!({ + "entity_id": id, + }), + Method::POST, + ) + .await?; + + loop { + let status = state.light_state(*component).await?; + if status == Status::Off { + break; + } + } + + Ok(Redirect::to("/")) +} + fn mk_port() -> u16 { 8080 } @@ -56,6 +174,8 @@ fn mk_port() -> u16 { struct Config { #[serde(default = "mk_port")] port: u16, + token: String, + ha_url: String, } #[tokio::main(flavor = "current_thread")] @@ -66,14 +186,22 @@ async fn main() { .init(); let config: Config = envious::Config::default() + .with_prefix("HAG_") .build_from_env() .expect("could not get env vars"); let templates = Tera::new(&format!("{TEMPLATE_DIR}/**/*.html")).unwrap(); - let state = Arc::new(AppState { templates }); + let state = Arc::new(AppState { + templates, + token: config.token, + ha_url: config.ha_url, + client: reqwest::Client::new(), + }); let router = Router::new() .route("/", get(index)) + .route("/on/:component", get(on)) + .route("/off/:component", get(off)) .with_state(state) .layer(TraceLayer::new_for_http()); diff --git a/templates/index.html b/templates/index.html index 9c6a2fc..f03385c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,7 +9,22 @@ integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous" /> - + +

Home Assistant Invité

+

Lumières

+
+ {% for light in ["salon"] %} + {% set st = status[light] %} + {% if st + == "on" %} + {% set action = "off" %} + {% else %} + {% set action = "on" %} + {% endif %} + {% set act = "/" ~ action ~ "/" ~ light %} + {{ light | capitalize }} ({{ st }}) + {% endfor %} +