Add webserver
This commit is contained in:
parent
e789c87372
commit
4c2addb2de
5 changed files with 195 additions and 19 deletions
|
@ -9,8 +9,9 @@ edition = "2021"
|
|||
rusqlite = "0.29.0"
|
||||
anyhow = { version = "1.0.71", features = ["backtrace"] }
|
||||
reqwest = "0.11.20"
|
||||
serde = { version="1.0", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
futures = "0.3.28"
|
||||
tokio = { version="1.32.0", features=["macros", "rt-multi-thread"] }
|
||||
tokio = { version = "1.32.0", features=["macros", "rt-multi-thread"] }
|
||||
chrono = "0.4.31"
|
||||
actix-web = "4.4.0"
|
|
@ -2,7 +2,7 @@ use std::fs;
|
|||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{from_str, to_string_pretty};
|
||||
use serde_json::{from_str, to_string, to_string_pretty};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Debug)]
|
||||
pub struct Devices {
|
||||
|
@ -14,6 +14,10 @@ impl Devices {
|
|||
Ok(from_str(&fs::read_to_string(file)?)?)
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<String> {
|
||||
Ok(to_string(self)?)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn save(&self, file: &str) -> Result<()> {
|
||||
fs::write(file, to_string_pretty(self)?)?;
|
||||
|
|
76
src/main.rs
76
src/main.rs
|
@ -10,16 +10,70 @@ mod data;
|
|||
mod db;
|
||||
mod devices;
|
||||
mod tasmota;
|
||||
mod web_server;
|
||||
|
||||
use actix_web::{
|
||||
get, middleware, rt,
|
||||
web::{self, Data},
|
||||
App, HttpRequest, HttpServer,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use devices::Devices;
|
||||
use futures::{future::try_join_all, try_join};
|
||||
use futures::{future::try_join_all, try_join, Future};
|
||||
use tasmota::Tasmota;
|
||||
use web_server::{device_query, index, plug_state};
|
||||
|
||||
fn since_epoch() -> Result<u64> {
|
||||
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
|
||||
}
|
||||
|
||||
fn read_power_usage(
|
||||
tasmota_plugs: Vec<Tasmota>,
|
||||
db: Arc<Mutex<DataBase>>,
|
||||
) -> impl Future<Output = Result<()>> {
|
||||
async move {
|
||||
loop {
|
||||
try_join_all(tasmota_plugs.iter().map(|plug| async {
|
||||
if let Ok(usage) = plug.read_power_usage().await {
|
||||
db.lock()
|
||||
.unwrap()
|
||||
.write(plug.name(), since_epoch()?, usage)?;
|
||||
|
||||
println!("read {}", plug.name(),);
|
||||
}
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}))
|
||||
.await?;
|
||||
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_web_server(
|
||||
devices: Devices,
|
||||
plugs: Vec<Tasmota>,
|
||||
db: Arc<Mutex<DataBase>>,
|
||||
) -> Result<()> {
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(Data::new(devices.clone()))
|
||||
.app_data(Data::new(db.clone()))
|
||||
.app_data(Data::new(plugs.clone()))
|
||||
.service(index)
|
||||
.service(device_query)
|
||||
.service(plug_state)
|
||||
.service(change_plug_state)
|
||||
})
|
||||
.bind(("127.0.0.1", 8062))
|
||||
.map_err(|err| anyhow::Error::msg(format!("failed binding to address: {err:#?}")))?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let db_future = DataBase::new("home_server.db");
|
||||
|
@ -28,25 +82,17 @@ async fn main() -> Result<()> {
|
|||
let (db, devices) = try_join!(db_future, devices_future)?;
|
||||
|
||||
let shared_db = Arc::new(Mutex::new(db));
|
||||
|
||||
let tasmota_plugs: Vec<Tasmota> = devices
|
||||
.plugs
|
||||
.iter()
|
||||
.map(|plug| Tasmota::new(plug))
|
||||
.collect();
|
||||
|
||||
loop {
|
||||
try_join_all(tasmota_plugs.iter().map(|plug| async {
|
||||
if let Ok(usage) = plug.read_power_usage().await {
|
||||
shared_db
|
||||
.lock()
|
||||
.unwrap()
|
||||
.write(plug.name(), since_epoch()?, usage)?;
|
||||
}
|
||||
let res = try_join!(
|
||||
read_power_usage(tasmota_plugs.clone(), shared_db.clone()),
|
||||
run_web_server(devices, tasmota_plugs, shared_db)
|
||||
)?;
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}))
|
||||
.await?;
|
||||
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
|||
use serde::Deserialize;
|
||||
use serde_json::from_str;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Tasmota {
|
||||
device: String,
|
||||
}
|
||||
|
|
124
src/web_server.rs
Normal file
124
src/web_server.rs
Normal file
|
@ -0,0 +1,124 @@
|
|||
use actix_web::{
|
||||
get, post,
|
||||
web::{Data, Json, Path},
|
||||
HttpRequest, Responder, ResponseError,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::{devices::Devices, tasmota::Tasmota};
|
||||
|
||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
|
||||
#[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("/")]
|
||||
async fn index(req: HttpRequest) -> &'static str {
|
||||
println!("REQ: {:?}", req);
|
||||
"Hello world!\r\n"
|
||||
}
|
||||
|
||||
#[get("/devices")]
|
||||
async fn device_query(devices: Data<Devices>) -> Result<impl Responder, impl ResponseError> {
|
||||
devices
|
||||
.to_json()
|
||||
.map(|json| Json(json))
|
||||
.map_err(|err| MyError::from(err))
|
||||
}
|
||||
|
||||
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(
|
||||
plug: Path<String>,
|
||||
action: 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) {
|
||||
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"),
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue