diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 139 |
1 files changed, 128 insertions, 11 deletions
diff --git a/src/main.rs b/src/main.rs index ad27759..d7dda15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,13 @@ #[macro_use] extern crate diesel; +use actix_files::Files; +use actix_web::http::header; use actix_web::{middleware, web, HttpResponse, HttpServer, Responder}; -use chrono::{Duration, Utc}; -use diesel::Connection; +use chrono::{DateTime, Duration, Utc}; use diesel::prelude::*; use diesel::sqlite::SqliteConnection; +use diesel::Connection; use hmac::{Hmac, Mac}; use serde::{Deserialize, Serialize}; use sha2::Sha256; @@ -13,9 +15,11 @@ use std::collections::BTreeMap; use std::io; use std::sync::Mutex; +mod maxmin; mod models; mod schema; +use self::maxmin::*; use self::models::*; /// Application context to be available on every request. @@ -24,10 +28,23 @@ struct Context { secret_key: Vec<u8>, } +/// Status of a device. +#[derive(Serialize)] +struct DeviceResponse { + device_id: String, + name: String, + battery_level: Option<f64>, + current_value: Option<f64>, + water_level: Option<f64>, + last_watered: Option<DateTime<Utc>>, + last_updated: Option<DateTime<Utc>>, +} + /// Time series of datapoints for a device. #[derive(Serialize)] struct DeviceData { - device: Device, + device_id: String, + device: DeviceResponse, data: Vec<Datapoint>, } @@ -46,10 +63,92 @@ struct PutDataRequest { signature: String, } +impl DeviceResponse { + pub fn calculate_status(&mut self, data: &[Datapoint]) { + self.last_updated = data + .last() + .map(|v| DateTime::<Utc>::from_utc(v.timestamp, Utc)); + self.battery_level = data.last().map(|v| v.battery_status); + + // Find the local extrema + let mut values = data.iter().map(|v| v.value).collect(); + normalize(&mut values); + smooth(&mut values, 3.0); + derive(&mut values); + let extrema = find_extrema(&values); + + // Look for a sharp drop in the value to indicate watering. + let last_watered_index = extrema + .iter() + .filter_map(|x| match x { + Extremum::Minimum(i) => { + if values[*i] < -0.1 { + Some(i) + } else { + None + } + } + _ => None, + }) + .last(); + + self.last_watered = + last_watered_index.map(|i| DateTime::<Utc>::from_utc(data[*i].timestamp, Utc)); + + // How much water is left? + let low_watermark = last_watered_index.map(|i| data[*i].value); + let high_watermark = last_watered_index + .and_then(|i| { + extrema + .iter() + .rev() + .filter_map(|x| match x { + Extremum::Maximum(j) => { + if j < i { + Some(j) + } else { + None + } + } + _ => None, + }) + .next() + }) + .map(|i| data[*i].value); + + self.current_value = data.last().map(|v| v.value); + self.water_level = if let (Some(high), Some(low), Some(current)) = + (high_watermark, low_watermark, self.current_value) + { + Some( + 1.0 - (current - f64::min(low, current)) + / (f64::max(high, current) - f64::max(low, current)), + ) + } else { + None + }; + } +} + +impl From<Device> for DeviceResponse { + fn from(device: Device) -> DeviceResponse { + DeviceResponse { + device_id: device.device_id, + name: device.name, + battery_level: None, + water_level: None, + current_value: None, + last_watered: None, + last_updated: None, + } + } +} + impl DeviceData { pub fn new(device: Device) -> Self { DeviceData { - device, + device_id: device.device_id.to_string(), + device: device.into(), data: Vec::new(), } } @@ -57,6 +156,10 @@ impl DeviceData { pub fn add_datapoint(&mut self, datapoint: Datapoint) { self.data.push(datapoint); } + + pub fn calculate_status(&mut self) { + self.device.calculate_status(&self.data); + } } impl DataResponse { @@ -74,7 +177,7 @@ impl DataResponse { impl PutDataRequest { pub fn validate_signature(&self, secret_key: &[u8]) -> bool { let data = format!( - "battery_value={}&device_id={}&value={}", + "battery_value={:0.2}&device_id={}&value={:0.2}", self.battery_value, urlencoding::encode(&self.device_id), self.value @@ -99,6 +202,11 @@ impl PutDataRequest { } }; + log::debug!( + "Verifying data '{}' matches signature '{}'", + &data, + &self.signature + ); mac.update(data.as_bytes()); mac.verify_slice(&signature).is_ok() } @@ -132,11 +240,15 @@ async fn get_data(ctx: web::Data<Context>) -> impl Responder { } let mut response = DataResponse::new(); - devicemap - .into_iter() - .for_each(|(_k, v)| response.add_device(v)); + devicemap.into_iter().for_each(|(_k, mut v)| { + v.calculate_status(); + response.add_device(v); + }); - HttpResponse::Ok().json(response) + HttpResponse::Ok() + .header(header::CACHE_CONTROL, "no-store") + .header(header::PRAGMA, "no-cache") + .json(response) } async fn put_data(req: web::Form<PutDataRequest>, ctx: web::Data<Context>) -> impl Responder { @@ -165,13 +277,17 @@ async fn put_data(req: web::Form<PutDataRequest>, ctx: web::Data<Context>) -> im device_id: req.device_id.to_string(), timestamp: Utc::now().naive_utc(), value: req.value, + battery_status: req.battery_value, }; diesel::insert_into(data::table) .values(&datapoint) .execute(&mut *db) .expect("Insert datapoint record"); - HttpResponse::Created().finish() + HttpResponse::Created() + .header(header::CACHE_CONTROL, "no-store") + .header(header::PRAGMA, "no-cache") + .finish() } fn open_database(db_filename: &str) -> io::Result<SqliteConnection> { @@ -183,7 +299,7 @@ async fn main() -> io::Result<()> { dotenv::dotenv().ok(); env_logger::init(); - let bind = std::env::var("BIND").unwrap_or_else(|_| "127.0.0.1:8180".to_string()); + let bind = std::env::var("BIND").unwrap_or_else(|_| ":::8180".to_string()); let db_path = std::env::var("DATABASE_URL").expect("Missing DATABASE env variable"); let secret_key = std::env::var("SECRET_KEY").expect("Missing SECRET_KEY env variable"); @@ -198,6 +314,7 @@ async fn main() -> io::Result<()> { .app_data(ctx.clone()) .route("/data", web::get().to(get_data)) .route("/data", web::put().to(put_data)) + .service(Files::new("/", "www/").index_file("index.html")) }) .bind(bind) .unwrap() |