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"
|
rusqlite = "0.29.0"
|
||||||
anyhow = { version = "1.0.71", features = ["backtrace"] }
|
anyhow = { version = "1.0.71", features = ["backtrace"] }
|
||||||
reqwest = "0.11.20"
|
reqwest = "0.11.20"
|
||||||
serde = { version="1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
futures = "0.3.28"
|
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"
|
chrono = "0.4.31"
|
||||||
|
actix-web = "4.4.0"
|
|
@ -2,7 +2,7 @@ use std::fs;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Debug)]
|
||||||
pub struct Devices {
|
pub struct Devices {
|
||||||
|
@ -14,6 +14,10 @@ impl Devices {
|
||||||
Ok(from_str(&fs::read_to_string(file)?)?)
|
Ok(from_str(&fs::read_to_string(file)?)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_json(&self) -> Result<String> {
|
||||||
|
Ok(to_string(self)?)
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn save(&self, file: &str) -> Result<()> {
|
pub fn save(&self, file: &str) -> Result<()> {
|
||||||
fs::write(file, to_string_pretty(self)?)?;
|
fs::write(file, to_string_pretty(self)?)?;
|
||||||
|
|
76
src/main.rs
76
src/main.rs
|
@ -10,16 +10,70 @@ mod data;
|
||||||
mod db;
|
mod db;
|
||||||
mod devices;
|
mod devices;
|
||||||
mod tasmota;
|
mod tasmota;
|
||||||
|
mod web_server;
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
get, middleware, rt,
|
||||||
|
web::{self, Data},
|
||||||
|
App, HttpRequest, HttpServer,
|
||||||
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use devices::Devices;
|
use devices::Devices;
|
||||||
use futures::{future::try_join_all, try_join};
|
use futures::{future::try_join_all, try_join, Future};
|
||||||
use tasmota::Tasmota;
|
use tasmota::Tasmota;
|
||||||
|
use web_server::{device_query, index, plug_state};
|
||||||
|
|
||||||
fn since_epoch() -> Result<u64> {
|
fn since_epoch() -> Result<u64> {
|
||||||
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let db_future = DataBase::new("home_server.db");
|
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 (db, devices) = try_join!(db_future, devices_future)?;
|
||||||
|
|
||||||
let shared_db = Arc::new(Mutex::new(db));
|
let shared_db = Arc::new(Mutex::new(db));
|
||||||
|
|
||||||
let tasmota_plugs: Vec<Tasmota> = devices
|
let tasmota_plugs: Vec<Tasmota> = devices
|
||||||
.plugs
|
.plugs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|plug| Tasmota::new(plug))
|
.map(|plug| Tasmota::new(plug))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
loop {
|
let res = try_join!(
|
||||||
try_join_all(tasmota_plugs.iter().map(|plug| async {
|
read_power_usage(tasmota_plugs.clone(), shared_db.clone()),
|
||||||
if let Ok(usage) = plug.read_power_usage().await {
|
run_web_server(devices, tasmota_plugs, shared_db)
|
||||||
shared_db
|
)?;
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.write(plug.name(), since_epoch()?, usage)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok(())
|
||||||
}))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
thread::sleep(Duration::from_secs(3));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ use anyhow::Result;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::from_str;
|
use serde_json::from_str;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Tasmota {
|
pub struct Tasmota {
|
||||||
device: String,
|
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