diff --git a/src/main.rs b/src/main.rs index 39f3244..01686ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,8 @@ async fn main() { let protected = Router::new() .route("/stats", get(routes::stats::get_stats)) .route("/power", get(routes::power::get_power)) + .route("/power/{device}/on", post(routes::power::power_on)) + .route("/power/{device}/off", post(routes::power::power_off)) .route( "/services/{service}/restart", post(routes::services::restart_service), diff --git a/src/routes/power.rs b/src/routes/power.rs index 4399eb3..9f0d7ce 100644 --- a/src/routes/power.rs +++ b/src/routes/power.rs @@ -1,8 +1,9 @@ +use axum::extract::Path; use axum::http::StatusCode; use axum::response::{IntoResponse, Json}; use tapo::ApiClient; -use crate::models; +use crate::{config, models}; async fn query_device( username: &str, @@ -24,6 +25,7 @@ async fn query_device( alias: info.nickname, model: info.model, on: info.device_on, + // current_power is in mW, convert to W current_power_w: energy.current_power.unwrap_or(0) as f64 / 1000.0, today_energy_wh: energy.today_energy, month_energy_wh: energy.month_energy, @@ -32,6 +34,26 @@ async fn query_device( }) } +fn credentials() -> Result<(String, String), (StatusCode, Json)> { + let u = std::env::var("TAPO_USERNAME").unwrap_or_default(); + let p = std::env::var("TAPO_PASSWORD").unwrap_or_default(); + if u.is_empty() || p.is_empty() { + Err(models::ActionResponse::err( + StatusCode::INTERNAL_SERVER_ERROR, + "TAPO_USERNAME / TAPO_PASSWORD not set", + )) + } else { + Ok((u, p)) + } +} + +fn resolve_device(name: &str) -> Option<&'static str> { + config::TAPO_DEVICES + .iter() + .find(|(n, _)| *n == name) + .map(|(_, ip)| *ip) +} + pub async fn get_power() -> impl IntoResponse { let username = std::env::var("TAPO_USERNAME").unwrap_or_default(); let password = std::env::var("TAPO_PASSWORD").unwrap_or_default(); @@ -44,7 +66,7 @@ pub async fn get_power() -> impl IntoResponse { .into_response(); } - let tasks: Vec<_> = crate::config::TAPO_DEVICES + let tasks: Vec<_> = config::TAPO_DEVICES .iter() .map(|(name, ip)| { let username = username.clone(); @@ -70,3 +92,49 @@ pub async fn get_power() -> impl IntoResponse { }) .into_response() } + +pub async fn power_on(Path(name): Path) -> impl IntoResponse { + let ip = match resolve_device(&name) { + Some(ip) => ip, + None => { + return models::ActionResponse::err( + StatusCode::NOT_FOUND, + &format!("unknown device '{name}'"), + ) + } + }; + let (username, password) = match credentials() { + Ok(c) => c, + Err(e) => return e, + }; + match ApiClient::new(&username, &password).p110(ip).await { + Err(e) => models::ActionResponse::err(StatusCode::BAD_GATEWAY, &format!("connect: {e}")), + Ok(device) => match device.on().await { + Ok(()) => models::ActionResponse::ok(format!("{name} turned on")), + Err(e) => models::ActionResponse::err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + }, + } +} + +pub async fn power_off(Path(name): Path) -> impl IntoResponse { + let ip = match resolve_device(&name) { + Some(ip) => ip, + None => { + return models::ActionResponse::err( + StatusCode::NOT_FOUND, + &format!("unknown device '{name}'"), + ) + } + }; + let (username, password) = match credentials() { + Ok(c) => c, + Err(e) => return e, + }; + match ApiClient::new(&username, &password).p110(ip).await { + Err(e) => models::ActionResponse::err(StatusCode::BAD_GATEWAY, &format!("connect: {e}")), + Ok(device) => match device.off().await { + Ok(()) => models::ActionResponse::ok(format!("{name} turned off")), + Err(e) => models::ActionResponse::err(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), + }, + } +}