diff options
| author | Jesse Morgan <jesse@jesterpm.net> | 2022-04-06 07:38:23 -0700 | 
|---|---|---|
| committer | Jesse Morgan <jesse@jesterpm.net> | 2022-04-06 07:38:23 -0700 | 
| commit | a34d968a08d6723968a5f1081d6bad0779875785 (patch) | |
| tree | ec0a327ff99317021d7995e45ad9ef120121fc34 | |
| parent | 462e9cca1d021652972067de39ff0118ce5faa2b (diff) | |
| -rw-r--r-- | .dockerignore | 2 | ||||
| -rw-r--r-- | Cargo.toml | 8 | ||||
| -rw-r--r-- | Dockerfile | 26 | ||||
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | README.md | 10 | ||||
| -rw-r--r-- | src/debug.rs | 75 | ||||
| -rw-r--r-- | src/device.rs | 141 | ||||
| -rw-r--r-- | src/main.rs | 99 | ||||
| -rw-r--r-- | src/maxmin.rs | 58 | ||||
| -rw-r--r-- | www/index.html | 10 | 
10 files changed, 322 insertions, 112 deletions
diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3ea0852 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +target/ +.git/ @@ -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 <jesse@jesterpm.net> + +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> { +    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::<Datapoint>(&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<f64>, +    current_value: Option<f64>, +    water_level: Option<f64>, +    last_watered: Option<DateTime<Utc>>, +    last_updated: Option<DateTime<Utc>>, +} + +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<f64> { +        self.battery_level +    } + +    pub fn current_value(&self) -> Option<f64> { +        self.current_value +    } + +    pub fn water_level(&self) -> Option<f64> { +        self.water_level +    } + +    pub fn last_watered(&self) -> Option<DateTime<Utc>> { +        self.last_watered +    } + +    pub fn last_updated(&self) -> Option<DateTime<Utc>> { +        self.last_updated +    } + +    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: Vec<f64> = 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::<Utc>::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<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, +        } +    } +} + 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<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 { @@ -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::<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, -        } -    } +    temperature_value: Option<f64>, +    relative_humidity_value: Option<f64>,  }  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<f64>) { +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<f64>) {      }  } -pub fn smooth(data: &mut Vec<f64>, 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<f64>, 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<f64>, sigma: f64) {      }  } -pub fn derive(data: &mut Vec<f64>) { -    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<f64>) {      // 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 @@              <p><img src="sadplant.png" alt="Sad flowerpot"></p>              <p>You don't have any plants :(</p>            </div> -          <div class="card"> -            <h2>A beautiful flower</h2> -            <p> -              <img src="icon.png"> -              <span class="level">75%</span> -            </p> -            <p class="detail"> -              Last watered Sunday, December 20th at 20:00 -            </p> -          </div>          </section>        </main>      </body>  | 
