Start frontend
This commit is contained in:
parent
4c2addb2de
commit
07b9b6c0be
10 changed files with 224 additions and 29 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -3,5 +3,6 @@
|
||||||
"activityBar.background": "#4C0C61",
|
"activityBar.background": "#4C0C61",
|
||||||
"titleBar.activeBackground": "#6B1188",
|
"titleBar.activeBackground": "#6B1188",
|
||||||
"titleBar.activeForeground": "#FDFAFE"
|
"titleBar.activeForeground": "#FDFAFE"
|
||||||
}
|
},
|
||||||
|
"rust-analyzer.showUnlinkedFileNotification": false
|
||||||
}
|
}
|
|
@ -15,3 +15,4 @@ 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"
|
actix-web = "4.4.0"
|
||||||
|
actix-files = "0.6.2"
|
1
build.sh
1
build.sh
|
@ -10,3 +10,4 @@ mkdir -p server
|
||||||
|
|
||||||
cp devices.conf server/
|
cp devices.conf server/
|
||||||
cp target/release/home_server server/
|
cp target/release/home_server server/
|
||||||
|
cp -r resources server/
|
0
resources/css/index.css
Normal file
0
resources/css/index.css
Normal file
0
resources/js/main.js
Normal file
0
resources/js/main.js
Normal file
14
resources/static/index.html
Normal file
14
resources/static/index.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Smart Homeserver</title>
|
||||||
|
<link href="/css/index.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="main"></div>
|
||||||
|
<script type="text/javascript" src="/js/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
144
src/db.rs
144
src/db.rs
|
@ -3,28 +3,52 @@ use std::path::Path;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use rusqlite::{Connection, ToSql};
|
use rusqlite::{Connection, ToSql};
|
||||||
|
|
||||||
|
use crate::devices::{Devices, DevicesWithName};
|
||||||
|
|
||||||
pub struct DataBase {
|
pub struct DataBase {
|
||||||
sql: Connection,
|
sql: Connection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DataBase {
|
impl DataBase {
|
||||||
|
const VERSION_0_1_0: &'static str = "0.1.0";
|
||||||
|
|
||||||
pub async fn new(path: impl AsRef<Path>) -> Result<Self> {
|
pub async fn new(path: impl AsRef<Path>) -> Result<Self> {
|
||||||
let me = Self {
|
let me = Self {
|
||||||
sql: Connection::open(path)?,
|
sql: Connection::open(path)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
me.generate_tables()?;
|
me.generate_tables()?;
|
||||||
|
me.init()?;
|
||||||
|
|
||||||
Ok(me)
|
Ok(me)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_tables(&self) -> Result<()> {
|
fn generate_tables(&self) -> Result<()> {
|
||||||
self.sql.execute(
|
self.sql.execute(
|
||||||
"CREATE TABLE IF NOT EXISTS data (
|
"CREATE TABLE IF NOT EXISTS meta (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
device TEXT NOT NULL,
|
version INTEGER NOT NULL
|
||||||
time BIGINT NOT NULL,
|
)",
|
||||||
watts REAL NOT NULL
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.sql.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS devices(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
device VARCHAR(60) NOT NULL,
|
||||||
|
type VARCHAR(30) NOT NULL,
|
||||||
|
name VARCHAR(80)
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.sql.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS data (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
time BIGINT NOT NULL,
|
||||||
|
watts REAL NOT NULL,
|
||||||
|
device_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(device_id) REFERENCES devices(id)
|
||||||
)",
|
)",
|
||||||
[],
|
[],
|
||||||
)?;
|
)?;
|
||||||
|
@ -32,18 +56,97 @@ impl DataBase {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn version(&self) -> Result<String> {
|
||||||
|
Ok(self
|
||||||
|
.sql
|
||||||
|
.query_row("SELECT version FROM meta WHERE id=1", [], |row| row.get(0))?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(&self) -> Result<()> {
|
||||||
|
if self.version().is_err() {
|
||||||
|
self.sql.execute(
|
||||||
|
"INSERT INTO meta (version)
|
||||||
|
VALUES (?1)",
|
||||||
|
&[Self::VERSION_0_1_0],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_devices(&self, devices: &Devices) -> Result<()> {
|
||||||
|
for device in devices.plugs.iter() {
|
||||||
|
self.sql.execute(
|
||||||
|
"INSERT INTO devices (device, type)
|
||||||
|
SELECT ?1, \"plug\"
|
||||||
|
WHERE
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT device
|
||||||
|
FROM devices
|
||||||
|
WHERE device=\"?1\"
|
||||||
|
)
|
||||||
|
",
|
||||||
|
&[device],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write(&self, device_name: &str, time: u64, watts: f32) -> Result<()> {
|
pub fn write(&self, device_name: &str, time: u64, watts: f32) -> Result<()> {
|
||||||
let params: &[&dyn ToSql] = &[&device_name, &time, &watts];
|
let params: &[&dyn ToSql] = &[&time, &watts, &device_name];
|
||||||
|
|
||||||
self.sql.execute(
|
self.sql.execute(
|
||||||
"INSERT INTO data (device, time, watts)
|
&format!(
|
||||||
VALUES (?1, ?2, ?3)",
|
"INSERT INTO data (time, watts, device_id)
|
||||||
|
VALUES (?1, ?2, (SELECT id FROM devices WHERE device=?3) )"
|
||||||
|
),
|
||||||
params,
|
params,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn devices(&self) -> Result<DevicesWithName> {
|
||||||
|
let mut devices = DevicesWithName::default();
|
||||||
|
|
||||||
|
for row in self
|
||||||
|
.sql
|
||||||
|
.prepare(&format!(
|
||||||
|
"
|
||||||
|
SELECT device, type, name
|
||||||
|
FROM devices
|
||||||
|
"
|
||||||
|
))?
|
||||||
|
.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?
|
||||||
|
{
|
||||||
|
let (device, dev_type, name): (String, String, String) = row?;
|
||||||
|
|
||||||
|
match dev_type.as_str() {
|
||||||
|
"plug" => devices.plugs.push((device, name)),
|
||||||
|
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn change_device_name(&self, device: &str, description: &str) -> Result<()> {
|
||||||
|
self.sql.execute(
|
||||||
|
&format!(
|
||||||
|
"
|
||||||
|
UPDATE devices
|
||||||
|
SET name=?1
|
||||||
|
WHERE device=?2
|
||||||
|
"
|
||||||
|
),
|
||||||
|
&[&description, device],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read(&self, device: &str) -> Result<Vec<(u64, f32)>> {
|
pub fn read(&self, device: &str) -> Result<Vec<(u64, f32)>> {
|
||||||
self.sql
|
self.sql
|
||||||
.prepare(&format!(
|
.prepare(&format!(
|
||||||
|
@ -69,22 +172,45 @@ mod test {
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::devices::Devices;
|
||||||
|
|
||||||
use super::DataBase;
|
use super::DataBase;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_connection() -> Result<()> {
|
async fn test_connection() -> Result<()> {
|
||||||
DataBase::new("connection_test.db").await?;
|
let db = DataBase::new("connection_test.db").await?;
|
||||||
|
assert_eq!(DataBase::VERSION_0_1_0, db.version()?);
|
||||||
|
|
||||||
fs::remove_file("connection_test.db")?;
|
fs::remove_file("connection_test.db")?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_startup() -> Result<()> {
|
||||||
|
let db = DataBase::new("startup_test.db").await?;
|
||||||
|
|
||||||
|
db.register_devices(&Devices {
|
||||||
|
plugs: vec!["test".to_string()],
|
||||||
|
})?;
|
||||||
|
|
||||||
|
fs::remove_file("startup_test.db")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_write() -> Result<()> {
|
async fn test_write() -> Result<()> {
|
||||||
let db = DataBase::new("write_test.db").await?;
|
let db = DataBase::new("write_test.db").await?;
|
||||||
|
|
||||||
db.write("test", 0, 5.5)?;
|
let device_name = "test";
|
||||||
|
|
||||||
|
db.register_devices(&Devices {
|
||||||
|
plugs: vec![device_name.to_string()],
|
||||||
|
})?;
|
||||||
|
|
||||||
|
db.write(device_name, 0, 5.5)?;
|
||||||
|
db.change_device_name(device_name, "udo")?;
|
||||||
|
|
||||||
fs::remove_file("write_test.db")?;
|
fs::remove_file("write_test.db")?;
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,17 @@ impl Devices {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||||
|
pub struct DevicesWithName {
|
||||||
|
pub plugs: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DevicesWithName {
|
||||||
|
pub fn to_json(&self) -> Result<String> {
|
||||||
|
Ok(to_string(self)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::Devices;
|
use super::Devices;
|
||||||
|
|
17
src/main.rs
17
src/main.rs
|
@ -12,16 +12,13 @@ mod devices;
|
||||||
mod tasmota;
|
mod tasmota;
|
||||||
mod web_server;
|
mod web_server;
|
||||||
|
|
||||||
use actix_web::{
|
use actix_files::Files;
|
||||||
get, middleware, rt,
|
use actix_web::{web::Data, App, HttpServer};
|
||||||
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, Future};
|
use futures::{future::try_join_all, try_join, Future};
|
||||||
use tasmota::Tasmota;
|
use tasmota::Tasmota;
|
||||||
use web_server::{device_query, index, plug_state};
|
use web_server::{change_plug_state, 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())
|
||||||
|
@ -38,8 +35,6 @@ fn read_power_usage(
|
||||||
db.lock()
|
db.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write(plug.name(), since_epoch()?, usage)?;
|
.write(plug.name(), since_epoch()?, usage)?;
|
||||||
|
|
||||||
println!("read {}", plug.name(),);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok::<(), anyhow::Error>(())
|
||||||
|
@ -61,6 +56,9 @@ async fn run_web_server(
|
||||||
.app_data(Data::new(devices.clone()))
|
.app_data(Data::new(devices.clone()))
|
||||||
.app_data(Data::new(db.clone()))
|
.app_data(Data::new(db.clone()))
|
||||||
.app_data(Data::new(plugs.clone()))
|
.app_data(Data::new(plugs.clone()))
|
||||||
|
.service(Files::new("/images", "resources/images/").show_files_listing())
|
||||||
|
.service(Files::new("/css", "resources/css").show_files_listing())
|
||||||
|
.service(Files::new("/js", "resources/js").show_files_listing())
|
||||||
.service(index)
|
.service(index)
|
||||||
.service(device_query)
|
.service(device_query)
|
||||||
.service(plug_state)
|
.service(plug_state)
|
||||||
|
@ -81,6 +79,7 @@ async fn main() -> Result<()> {
|
||||||
|
|
||||||
let (db, devices) = try_join!(db_future, devices_future)?;
|
let (db, devices) = try_join!(db_future, devices_future)?;
|
||||||
|
|
||||||
|
db.register_devices(&devices)?;
|
||||||
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
|
||||||
|
@ -89,7 +88,7 @@ async fn main() -> Result<()> {
|
||||||
.map(|plug| Tasmota::new(plug))
|
.map(|plug| Tasmota::new(plug))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let res = try_join!(
|
try_join!(
|
||||||
read_power_usage(tasmota_plugs.clone(), shared_db.clone()),
|
read_power_usage(tasmota_plugs.clone(), shared_db.clone()),
|
||||||
run_web_server(devices, tasmota_plugs, shared_db)
|
run_web_server(devices, tasmota_plugs, shared_db)
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
|
use actix_files::NamedFile;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
get, post,
|
get, post,
|
||||||
web::{Data, Json, Path},
|
web::{Data, Json, Path},
|
||||||
HttpRequest, Responder, ResponseError,
|
Responder, ResponseError,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::to_string;
|
use serde_json::to_string;
|
||||||
|
|
||||||
use crate::{devices::Devices, tasmota::Tasmota};
|
use crate::{db::DataBase, tasmota::Tasmota};
|
||||||
|
|
||||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
use std::{
|
||||||
|
fmt::{Display, Formatter, Result as FmtResult},
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct DeviceState {
|
struct DeviceState {
|
||||||
|
@ -40,19 +44,36 @@ impl From<anyhow::Error> for MyError {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn index(req: HttpRequest) -> &'static str {
|
async fn index() -> Result<NamedFile, impl ResponseError> {
|
||||||
println!("REQ: {:?}", req);
|
NamedFile::open("resources/static/index.html")
|
||||||
"Hello world!\r\n"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/devices")]
|
#[get("/devices")]
|
||||||
async fn device_query(devices: Data<Devices>) -> Result<impl Responder, impl ResponseError> {
|
async fn device_query(
|
||||||
devices
|
db: Data<Arc<Mutex<DataBase>>>,
|
||||||
|
) -> Result<impl Responder, impl ResponseError> {
|
||||||
|
db.lock()
|
||||||
|
.unwrap()
|
||||||
|
.devices()?
|
||||||
.to_json()
|
.to_json()
|
||||||
.map(|json| Json(json))
|
.map(|json| Json(json))
|
||||||
.map_err(|err| MyError::from(err))
|
.map_err(|err| MyError::from(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/device_name/{device}/{name}")]
|
||||||
|
async fn change_device_name(
|
||||||
|
device: Path<String>,
|
||||||
|
name: Path<String>,
|
||||||
|
db: Data<Arc<Mutex<DataBase>>>,
|
||||||
|
) -> Result<impl Responder, MyError> {
|
||||||
|
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<String> {
|
async fn tasmota_info(tasmota: &Tasmota) -> anyhow::Result<String> {
|
||||||
let led = tasmota.led_state().await?;
|
let led = tasmota.led_state().await?;
|
||||||
let power = tasmota.power_state().await?;
|
let power = tasmota.power_state().await?;
|
||||||
|
@ -122,3 +143,24 @@ async fn change_plug_state(
|
||||||
msg: format!("plug ({plug_name}) not found"),
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue