diff --git a/.vscode/settings.json b/.vscode/settings.json
index d7e496a..7f09cfc 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,5 +3,6 @@
"activityBar.background": "#4C0C61",
"titleBar.activeBackground": "#6B1188",
"titleBar.activeForeground": "#FDFAFE"
- }
+ },
+ "rust-analyzer.showUnlinkedFileNotification": false
}
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index aa82ce1..914ddb2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,4 +14,5 @@ serde_json = "1.0"
futures = "0.3.28"
tokio = { version = "1.32.0", features=["macros", "rt-multi-thread"] }
chrono = "0.4.31"
-actix-web = "4.4.0"
\ No newline at end of file
+actix-web = "4.4.0"
+actix-files = "0.6.2"
\ No newline at end of file
diff --git a/build.sh b/build.sh
index 5ae879c..bf3bc68 100644
--- a/build.sh
+++ b/build.sh
@@ -9,4 +9,5 @@ cargo build --release
mkdir -p server
cp devices.conf server/
-cp target/release/home_server server/
\ No newline at end of file
+cp target/release/home_server server/
+cp -r resources server/
\ No newline at end of file
diff --git a/resources/css/index.css b/resources/css/index.css
new file mode 100644
index 0000000..e69de29
diff --git a/resources/js/main.js b/resources/js/main.js
new file mode 100644
index 0000000..e69de29
diff --git a/resources/static/index.html b/resources/static/index.html
new file mode 100644
index 0000000..96c5206
--- /dev/null
+++ b/resources/static/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+ Smart Homeserver
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/db.rs b/src/db.rs
index f547a00..d085010 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -3,28 +3,52 @@ use std::path::Path;
use anyhow::Result;
use rusqlite::{Connection, ToSql};
+use crate::devices::{Devices, DevicesWithName};
+
pub struct DataBase {
sql: Connection,
}
impl DataBase {
+ const VERSION_0_1_0: &'static str = "0.1.0";
+
pub async fn new(path: impl AsRef) -> Result {
let me = Self {
sql: Connection::open(path)?,
};
me.generate_tables()?;
+ me.init()?;
Ok(me)
}
fn generate_tables(&self) -> Result<()> {
self.sql.execute(
- "CREATE TABLE IF NOT EXISTS data (
+ "CREATE TABLE IF NOT EXISTS meta (
id INTEGER PRIMARY KEY,
- device TEXT NOT NULL,
- time BIGINT NOT NULL,
- watts REAL NOT NULL
+ version INTEGER 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(())
}
+ pub fn version(&self) -> Result {
+ 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<()> {
- let params: &[&dyn ToSql] = &[&device_name, &time, &watts];
+ let params: &[&dyn ToSql] = &[&time, &watts, &device_name];
self.sql.execute(
- "INSERT INTO data (device, time, watts)
- VALUES (?1, ?2, ?3)",
+ &format!(
+ "INSERT INTO data (time, watts, device_id)
+ VALUES (?1, ?2, (SELECT id FROM devices WHERE device=?3) )"
+ ),
params,
)?;
Ok(())
}
+ pub fn devices(&self) -> Result {
+ 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> {
self.sql
.prepare(&format!(
@@ -69,22 +172,45 @@ mod test {
use anyhow::Result;
+ use crate::devices::Devices;
+
use super::DataBase;
#[tokio::test]
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")?;
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]
async fn test_write() -> Result<()> {
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")?;
diff --git a/src/devices.rs b/src/devices.rs
index 148d8cb..6132c8f 100644
--- a/src/devices.rs
+++ b/src/devices.rs
@@ -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 {
+ Ok(to_string(self)?)
+ }
+}
+
#[cfg(test)]
mod test {
use super::Devices;
diff --git a/src/main.rs b/src/main.rs
index b71a46e..b5c13d9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,16 +12,13 @@ mod devices;
mod tasmota;
mod web_server;
-use actix_web::{
- get, middleware, rt,
- web::{self, Data},
- App, HttpRequest, HttpServer,
-};
+use actix_files::Files;
+use actix_web::{web::Data, App, HttpServer};
use anyhow::Result;
use devices::Devices;
use futures::{future::try_join_all, try_join, Future};
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 {
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
@@ -38,8 +35,6 @@ fn read_power_usage(
db.lock()
.unwrap()
.write(plug.name(), since_epoch()?, usage)?;
-
- println!("read {}", plug.name(),);
}
Ok::<(), anyhow::Error>(())
@@ -61,6 +56,9 @@ async fn run_web_server(
.app_data(Data::new(devices.clone()))
.app_data(Data::new(db.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(device_query)
.service(plug_state)
@@ -81,6 +79,7 @@ async fn main() -> Result<()> {
let (db, devices) = try_join!(db_future, devices_future)?;
+ db.register_devices(&devices)?;
let shared_db = Arc::new(Mutex::new(db));
let tasmota_plugs: Vec = devices
@@ -89,7 +88,7 @@ async fn main() -> Result<()> {
.map(|plug| Tasmota::new(plug))
.collect();
- let res = try_join!(
+ try_join!(
read_power_usage(tasmota_plugs.clone(), shared_db.clone()),
run_web_server(devices, tasmota_plugs, shared_db)
)?;
diff --git a/src/web_server.rs b/src/web_server.rs
index 8d342ed..8023a2e 100644
--- a/src/web_server.rs
+++ b/src/web_server.rs
@@ -1,14 +1,18 @@
+use actix_files::NamedFile;
use actix_web::{
get, post,
web::{Data, Json, Path},
- HttpRequest, Responder, ResponseError,
+ Responder, ResponseError,
};
use serde::Serialize;
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)]
struct DeviceState {
@@ -40,19 +44,36 @@ impl From for MyError {
}
#[get("/")]
-async fn index(req: HttpRequest) -> &'static str {
- println!("REQ: {:?}", req);
- "Hello world!\r\n"
+async fn index() -> Result {
+ NamedFile::open("resources/static/index.html")
}
#[get("/devices")]
-async fn device_query(devices: Data) -> Result {
- 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?;
@@ -122,3 +143,24 @@ async fn change_plug_state(
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);
+ }
+}