Allow to toggle living room light

This commit is contained in:
Quentin Boyer 2023-11-04 18:22:10 +01:00
parent 48bb33e2bc
commit aac937ad01
4 changed files with 492 additions and 7 deletions

340
Cargo.lock generated
View file

@ -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",
]

View file

@ -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"] }

View file

@ -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<Arc<AppState>>;
#[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<Html<String>, 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<D, R>(&self, path: &str, data: &D, method: Method) -> Result<R, Error>
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<Status, Error> {
#[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<Component>) -> Result<Redirect, Error> {
let id = component.light_id();
state
.ha_request::<_, Vec<Empty>>(
"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<Component>) -> Result<Redirect, Error> {
let id = component.light_id();
state
.ha_request::<_, Vec<Empty>>(
"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());

View file

@ -9,7 +9,22 @@
integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"
crossorigin="anonymous" />
</head>
<body>
<body class="container">
<h1>Home Assistant Invité</h1>
<h2>Lumières</h2>
<div class="list-group">
{% for light in ["salon"] %}
{% set st = status[light] %}
{% if st
== "on" %}
{% set action = "off" %}
{% else %}
{% set action = "on" %}
{% endif %}
{% set act = "/" ~ action ~ "/" ~ light %}
<a href="{{ act }}" class="list-group-item list-group-action">{{ light | capitalize }} ({{ st }})</a>
{% endfor %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
crossorigin="anonymous"></script>