summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/debug.rs75
-rw-r--r--src/device.rs141
-rw-r--r--src/main.rs99
-rw-r--r--src/maxmin.rs58
4 files changed, 271 insertions, 102 deletions
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);
+ }
}