summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dockerignore2
-rw-r--r--Cargo.toml8
-rw-r--r--Dockerfile26
-rw-r--r--Makefile5
-rw-r--r--README.md10
-rw-r--r--src/debug.rs75
-rw-r--r--src/device.rs141
-rw-r--r--src/main.rs99
-rw-r--r--src/maxmin.rs58
-rw-r--r--www/index.html10
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/
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 <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>