HomeServer/src/web_server.rs

233 lines
5.6 KiB
Rust
Raw Normal View History

2023-09-21 08:08:23 +00:00
use actix_files::NamedFile;
2023-09-21 05:30:39 +00:00
use actix_web::{
get, post,
web::{Data, Json, Path},
2023-09-21 08:08:23 +00:00
Responder, ResponseError,
2023-09-21 05:30:39 +00:00
};
use serde::Serialize;
use serde_json::to_string;
2023-09-21 08:08:23 +00:00
use crate::{db::DataBase, tasmota::Tasmota};
2023-09-21 05:30:39 +00:00
2023-09-21 08:08:23 +00:00
use std::{
fmt::{Display, Formatter, Result as FmtResult},
sync::{Arc, Mutex},
};
2023-09-21 05:30:39 +00:00
#[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<anyhow::Error> for MyError {
fn from(value: anyhow::Error) -> Self {
MyError {
msg: value.to_string(),
}
}
}
#[get("/")]
2023-09-21 08:08:23 +00:00
async fn index() -> Result<NamedFile, impl ResponseError> {
NamedFile::open("resources/static/index.html")
2023-09-21 05:30:39 +00:00
}
#[get("/devices")]
2023-09-21 08:08:23 +00:00
async fn device_query(
db: Data<Arc<Mutex<DataBase>>>,
) -> Result<impl Responder, impl ResponseError> {
db.lock()
.unwrap()
2023-09-21 08:46:23 +00:00
.devices()
.map_err(|err| {
println!("{err:?}");
MyError::from(err)
})?
2023-09-21 05:30:39 +00:00
.to_json()
.map(|json| Json(json))
2023-09-21 08:46:23 +00:00
.map_err(|err| {
println!("{err:?}");
MyError::from(err)
})
2023-09-21 05:30:39 +00:00
}
2023-09-21 08:08:23 +00:00
#[post("/device_name/{device}/{name}")]
async fn change_device_name(
2023-09-21 11:32:44 +00:00
param: Path<(String, String)>,
2023-09-21 08:08:23 +00:00
db: Data<Arc<Mutex<DataBase>>>,
) -> Result<impl Responder, MyError> {
2023-09-21 11:32:44 +00:00
let (device, name) = param.into_inner();
2023-09-21 08:08:23 +00:00
db.lock()
.unwrap()
2023-09-21 11:32:44 +00:00
.change_device_name(&device, &name)
2023-09-21 08:08:23 +00:00
.map_err(|err| MyError::from(err))?;
return Ok("Ok");
}
2023-09-21 05:30:39 +00:00
async fn tasmota_info(tasmota: &Tasmota) -> anyhow::Result<String> {
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<String>,
plugs: Data<Vec<Tasmota>>,
) -> Result<impl Responder, impl ResponseError> {
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(
2023-09-21 11:32:44 +00:00
param: Path<(String, String)>,
2023-09-21 05:30:39 +00:00
plugs: Data<Vec<Tasmota>>,
) -> Result<impl Responder, impl ResponseError> {
2023-09-21 11:32:44 +00:00
let (plug_name, action_type) = param.into_inner();
2023-09-21 05:30:39 +00:00
if let Some(tasmota) = plugs.iter().find(|tasmota| tasmota.name() == plug_name) {
2023-09-21 11:32:44 +00:00
match action_type.as_str() {
2023-09-21 05:30:39 +00:00
"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"),
})
}
2023-09-21 08:08:23 +00:00
#[cfg(test)]
mod test {
use actix_web::{http::header::ContentType, test, App};
2023-09-21 11:32:44 +00:00
use reqwest::Method;
use std::{thread, time::Duration};
2023-09-21 08:08:23 +00:00
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();
2023-09-21 11:32:44 +00:00
assert!(
status.is_success(),
"status: {:?}, error: {:?}",
status,
body
);
}
#[actix_web::test]
async fn test_led_on_off() {
let app = test::init_service(
App::new()
.service(change_plug_state)
.app_data(Data::new(vec![Tasmota::new("Tasmota-Plug-3")])),
)
.await;
{
let req = test::TestRequest::default()
.uri("/plug/Tasmota-Plug-3/led_off")
.insert_header(ContentType::plaintext())
.method(Method::POST)
.to_request();
let resp = test::call_service(&app, req).await;
let status = resp.status();
let body = resp.into_body();
assert!(
status.is_success(),
"status: {:?}, error: {:?}",
status,
body
);
}
thread::sleep(Duration::from_secs(5));
{
let req = test::TestRequest::default()
.uri("/plug/Tasmota-Plug-3/led_on")
.insert_header(ContentType::plaintext())
.method(Method::POST)
.to_request();
let resp = test::call_service(&app, req).await;
let status = resp.status();
let body = resp.into_body();
assert!(
status.is_success(),
"status: {:?}, error: {:?}",
status,
body
);
}
2023-09-21 08:08:23 +00:00
}
}