use actix_files::NamedFile; use actix_web::{ get, post, web::{Data, Json, Path}, Responder, ResponseError, }; use serde::Serialize; use serde_json::to_string; use crate::{db::DataBase, tasmota::Tasmota}; use std::{ fmt::{Display, Formatter, Result as FmtResult}, sync::{Arc, Mutex}, }; #[derive(Serialize)] struct DeviceState { name: String, power: bool, led: bool, power_draw: f32, } #[derive(Debug)] struct MyError { msg: String, } impl Display for MyError { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { write!(f, "Error: {}", self.msg) } } impl ResponseError for MyError {} impl From for MyError { fn from(value: anyhow::Error) -> Self { MyError { msg: value.to_string(), } } } #[get("/")] async fn index() -> Result { NamedFile::open("resources/static/index.html") } #[get("/devices")] async fn device_query( db: Data>>, ) -> Result { db.lock() .unwrap() .devices()? .to_json() .map(|json| Json(json)) .map_err(|err| MyError::from(err)) } #[post("/device_name/{device}/{name}")] async fn change_device_name( device: Path, name: Path, db: Data>>, ) -> Result { db.lock() .unwrap() .change_device_name(&device.into_inner(), &name.into_inner()) .map_err(|err| MyError::from(err))?; return Ok("Ok"); } async fn tasmota_info(tasmota: &Tasmota) -> anyhow::Result { let led = tasmota.led_state().await?; let power = tasmota.power_state().await?; let power_draw = tasmota.read_power_usage().await?; Ok(to_string(&DeviceState { name: tasmota.name().to_string(), power, led, power_draw, })?) } #[get("/plug_state/{plug}")] async fn plug_state( plug: Path, plugs: Data>, ) -> Result { let plug_name = plug.into_inner(); if let Some(tasmota) = plugs.iter().find(|tasmota| tasmota.name() == plug_name) { return Ok(tasmota_info(tasmota) .await .map(|s| Json(s)) .map_err(|err| MyError::from(err))?); } Err(MyError { msg: format!("plug ({plug_name}) not found"), }) } #[post("/plug/{plug}/{action}")] async fn change_plug_state( plug: Path, action: Path, plugs: Data>, ) -> Result { let plug_name = plug.into_inner(); if let Some(tasmota) = plugs.iter().find(|tasmota| tasmota.name() == plug_name) { match action.into_inner().as_str() { "led_on" => tasmota .turn_on_led() .await .map_err(|err| MyError::from(err))?, "led_off" => tasmota .turn_off_led() .await .map_err(|err| MyError::from(err))?, "power_on" => tasmota .switch_on() .await .map_err(|err| MyError::from(err))?, "power_off" => tasmota .switch_off() .await .map_err(|err| MyError::from(err))?, _ => (), } return Ok("Ok"); } Err(MyError { msg: format!("plug ({plug_name}) not found"), }) } #[cfg(test)] mod test { use actix_web::{http::header::ContentType, test, App}; use super::*; #[actix_web::test] async fn test_index_get() { let app = test::init_service(App::new().service(index)).await; let req = test::TestRequest::default() .insert_header(ContentType::plaintext()) .to_request(); let resp = test::call_service(&app, req).await; let status = resp.status(); let body = resp.into_body(); assert!(status.is_success(), "{:?}", body); } }