From a34d968a08d6723968a5f1081d6bad0779875785 Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Wed, 6 Apr 2022 07:38:23 -0700 Subject: Snapshot of progress post-christmas --- .dockerignore | 2 + Cargo.toml | 8 ++++ Dockerfile | 26 +++++++++++ Makefile | 5 ++ README.md | 10 ++++ src/debug.rs | 75 ++++++++++++++++++++++++++++++ src/device.rs | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 99 ++-------------------------------------- src/maxmin.rs | 58 ++++++++++++++++++++---- www/index.html | 10 ---- 10 files changed, 322 insertions(+), 112 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 src/debug.rs create mode 100644 src/device.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3ea0852 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +target/ +.git/ diff --git a/Cargo.toml b/Cargo.toml index 3478efb..9ed75f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,14 @@ name = "flowerpot" version = "0.1.0" edition = "2021" +[[bin]] +name = "flowerpot" +path = "src/main.rs" + +[[bin]] +name = "flowerpot-debug" +path = "src/debug.rs" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c1354eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Based on https://alexbrand.dev/post/how-to-package-rust-applications-into-minimal-docker-containers/ +FROM rust:1.57.0 AS build + +MAINTAINER Jesse Morgan + +WORKDIR /usr/src + +# Build the dependencies first +# This should help repeated builds go faster. +RUN USER=root cargo new flowerpot +WORKDIR /usr/src/flowerpot +COPY Cargo.toml Cargo.lock ./ +RUN cargo build --release + +# Copy the source and build the application. +COPY src ./src +RUN cargo install --path . + +# Now build the deployment image. +FROM debian:stable-slim +# RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y libssl1.1 ca-certificates sqlite3 +COPY --from=build /usr/local/cargo/bin/flowerpot . +COPY www ./www +USER 999 +CMD ["./flowerpot"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a53b347 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +deploy: + docker build -t flowerpot . + $(aws ecr get-login --no-include-email) + docker tag flowerpot:latest ${AWS_ACCOUNT_ID}.dkr.ecr.us-west-2.amazonaws.com/flowerpot:prod + docker push ${AWS_ACCOUNT_ID}.dkr.ecr.us-west-2.amazonaws.com/flowerpot:prod diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f2a904 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +### Building and Deploying + + docker build -t flowerpot . + + $(aws ecr get-login --no-include-email) + + docker tag flowerpot:latest ${AWS_ACCOUNT_ID}.dkr.ecr.us-west-2.amazonaws.com/flowerpot:prod + + docker push ${AWS_ACCOUNT_ID}.dkr.ecr.us-west-2.amazonaws.com/flowerpot:prod + diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..efcee52 --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,75 @@ +#[macro_use] +extern crate diesel; + +use chrono::{DateTime, Duration, Utc}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; +use diesel::Connection; +use std::io; + +mod maxmin; +mod models; +mod schema; +mod device; + +use self::maxmin::*; +use self::models::*; +use self::device::DeviceResponse; + +fn open_database(db_filename: &str) -> io::Result { + SqliteConnection::establish(db_filename).map_err(|e| io::Error::new(io::ErrorKind::Other, e)) +} + +fn main() -> io::Result<()> { + use self::schema::data::dsl::*; + use self::schema::devices::dsl::*; + + dotenv::dotenv().ok(); + env_logger::init(); + + let db_path = std::env::var("DATABASE_URL").expect("Missing DATABASE env variable"); + let given_device_id = std::env::var("DEVICE_ID").expect("Missing DEVICE_ID env variable"); + + let mut db = open_database(&db_path)?; + + let start_time = Utc::now() - Duration::days(90); + let data_records = data + .filter(timestamp.ge(start_time.naive_utc()).and(self::schema::data::dsl::device_id.eq(&given_device_id))) + .load::(&mut db) + .expect("Error loading data"); + + let mut resp = DeviceResponse::new(given_device_id); + + println!( + "{:<40}\t{:>8}\t{:<20?}\t{:<40}", + "timestamp", + "value", + "water_level", + "last_watered"); + + for (i, record) in data_records.iter().enumerate() { + let previous_last_watered = resp.last_watered(); + + resp.calculate_status(&data_records[0..i]); + + let flag = if resp.last_watered() != previous_last_watered { + "******" + } else if record.value < 12000.0 { + "^^^^^" + } else { + "" + }; + + println!( + "{:<40}\t{:>8}\t{:<20?}\t{:<40?} {}", + record.timestamp, + record.value, + resp.water_level(), + resp.last_watered(), + flag, + ); + } + + Ok(()) +} + diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..b62b633 --- /dev/null +++ b/src/device.rs @@ -0,0 +1,141 @@ +use chrono::{DateTime, Duration, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::maxmin::*; +use crate::models::*; + +/// Status of a device. +#[derive(Serialize)] +pub struct DeviceResponse { + device_id: String, + name: String, + battery_level: Option, + current_value: Option, + water_level: Option, + last_watered: Option>, + last_updated: Option>, +} + +impl DeviceResponse { + pub fn new(device_id: String) -> Self { + DeviceResponse { + name: device_id.to_string(), + device_id: device_id, + battery_level: None, + water_level: None, + current_value: None, + last_watered: None, + last_updated: None, + } + } + + pub fn device_id(&self) -> &str { + &self.device_id + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn battery_level(&self) -> Option { + self.battery_level + } + + pub fn current_value(&self) -> Option { + self.current_value + } + + pub fn water_level(&self) -> Option { + self.water_level + } + + pub fn last_watered(&self) -> Option> { + self.last_watered + } + + pub fn last_updated(&self) -> Option> { + self.last_updated + } + + pub fn calculate_status(&mut self, data: &[Datapoint]) { + self.last_updated = data + .last() + .map(|v| DateTime::::from_utc(v.timestamp, Utc)); + self.battery_level = data.last().map(|v| v.battery_status); + + // Find the local extrema + let mut values: Vec = data.iter().map(|v| v.value).collect(); + clamp(&mut values, 1000.0, 300000.0); + normalize(&mut values); + smooth(&mut values, 2.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 1 == 1 || values[*i] < -0.1 { + Some(i) + } else { + None + } + } + _ => None, + }) + .last(); + + self.last_watered = + last_watered_index.map(|i| DateTime::::from_utc(data[*i].timestamp, Utc)); + + // How much water is left? + let low_watermark = f64::min(1000.0, last_watered_index.map(|i| data[*i].value) + .unwrap_or(10000.0)); + + let high_watermark = f64::min(300000.0, 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) + .unwrap_or(300000.0)); + + self.current_value = data.last().map(|v| v.value); + self.water_level = if let Some(current) = self.current_value { + Some( + 1.0 - (current - f64::min(low_watermark, current)) + / (f64::max(high_watermark, current) - f64::max(low_watermark, current)), + ) + } else { + None + }; + } +} + +impl From 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, + } + } +} + diff --git a/src/main.rs b/src/main.rs index d7dda15..19880cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,9 +18,11 @@ use std::sync::Mutex; mod maxmin; mod models; mod schema; +mod device; use self::maxmin::*; use self::models::*; +use self::device::DeviceResponse; /// Application context to be available on every request. struct Context { @@ -28,18 +30,6 @@ struct Context { secret_key: Vec, } -/// Status of a device. -#[derive(Serialize)] -struct DeviceResponse { - device_id: String, - name: String, - battery_level: Option, - current_value: Option, - water_level: Option, - last_watered: Option>, - last_updated: Option>, -} - /// Time series of datapoints for a device. #[derive(Serialize)] struct DeviceData { @@ -61,87 +51,8 @@ struct PutDataRequest { value: f64, battery_value: f64, signature: String, -} - -impl DeviceResponse { - pub fn calculate_status(&mut self, data: &[Datapoint]) { - self.last_updated = data - .last() - .map(|v| DateTime::::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::::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 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, - } - } + temperature_value: Option, + relative_humidity_value: Option, } impl DeviceData { @@ -314,7 +225,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")) + .service(Files::new("/", "./www").index_file("index.html")) }) .bind(bind) .unwrap() diff --git a/src/maxmin.rs b/src/maxmin.rs index 6bea2d7..df4a0f5 100644 --- a/src/maxmin.rs +++ b/src/maxmin.rs @@ -1,6 +1,16 @@ use std::cmp::Ordering; -pub fn normalize(data: &mut Vec) { +pub fn clamp(data: &mut [f64], lower: f64, upper: f64) { + if data.is_empty() { + return; + } + + for x in data.iter_mut() { + *x = f64::min(upper, f64::max(lower, *x)); + } +} + +pub fn normalize(data: &mut [f64]) { if data.is_empty() { return; } @@ -18,8 +28,8 @@ pub fn normalize(data: &mut Vec) { } } -pub fn smooth(data: &mut Vec, sigma: f64) { - const WINDOW_SIZE: usize = 7; +pub fn smooth(data: &mut [f64], sigma: f64) { + const WINDOW_SIZE: usize = 5; if data.is_empty() { return; @@ -40,7 +50,7 @@ pub fn smooth(data: &mut Vec, sigma: f64) { // Smooth data let first = data.first().unwrap(); - let mut buffer = [*first; WINDOW_SIZE]; + let mut buffer = [f64::NAN; WINDOW_SIZE]; let mut bi = 0; for (_loc, x) in data.iter_mut().enumerate() { buffer[bi] = *x; @@ -52,8 +62,8 @@ pub fn smooth(data: &mut Vec, sigma: f64) { } } -pub fn derive(data: &mut Vec) { - const WINDOW_SIZE: usize = 3; +pub fn derive(data: &mut [f64]) { + const WINDOW_SIZE: usize = 5; if data.is_empty() { return; @@ -61,12 +71,12 @@ pub fn derive(data: &mut Vec) { // Calculate the derivative. let first = data.first().unwrap(); - let mut buffer = [*first; WINDOW_SIZE]; + let mut buffer = [f64::NAN; WINDOW_SIZE]; let mut bi = 0; for (_loc, x) in data.iter_mut().enumerate() { buffer[bi] = *x; *x = (buffer[bi] - buffer[(bi + WINDOW_SIZE - 1) % (WINDOW_SIZE)]) - / (WINDOW_SIZE as f64 - 1.0); + / (WINDOW_SIZE as f64); bi = (bi + 1) % WINDOW_SIZE; } } @@ -142,4 +152,36 @@ mod tests { assert!((0.667 - x[2]).abs() < 0.001); assert!((1.0 - x[3]).abs() < 0.001); } + + + #[test] + fn first_run() { + + + // let mut values = [37200000.0, 13323636.0, 10937.16, 12403.11, 12771.62, 12771.62, 13511.11, 13140.96]; + let mut values = [13323636.0, 10937.16, 12403.11, 12771.62, 12771.62, 13511.11, 13140.96]; + for (i, x) in values.iter().enumerate() { + println!("original: {} => {:?}", i, x); + } + super::normalize(&mut values); + for (i, x) in values.iter().enumerate() { + println!("normalize: {} => {:?}", i, x); + } + // super::smooth(&mut values, 1.0); + // for (i, x) in values.iter().enumerate() { + // println!("smooth: {} => {:?}", i, x); + // } + super::derive(&mut values); + for (i, x) in values.iter().enumerate() { + println!("derive: {} => {:?}", i, x); + } + let extrema = super::find_extrema(&values); + + + for x in extrema { + println!("{:?}", x); + } + + assert!(false); + } } diff --git a/www/index.html b/www/index.html index 44592c0..85cce07 100644 --- a/www/index.html +++ b/www/index.html @@ -63,16 +63,6 @@

Sad flowerpot

You don't have any plants :(

-
-

A beautiful flower

-

- - 75% -

-

- Last watered Sunday, December 20th at 20:00 -

-
-- cgit v1.2.3