use anyhow::{Context, Result};
use chrono::DateTime;
use serde::Deserialize;
use serde_json::Value;
use crate::config::Site;
const PRED_STEP_MS: i64 = 5 * 60_000;
#[derive(Debug, Clone, Deserialize)]
pub struct Entry {
pub sgv: f64,
pub date: i64,
#[serde(default)]
pub direction: Option<String>,
}
impl Entry {
pub fn arrow(&self) -> &'static str {
match self.direction.as_deref() {
Some("DoubleUp") => "⇈",
Some("SingleUp") => "↑",
Some("FortyFiveUp") => "↗",
Some("Flat") => "→",
Some("FortyFiveDown") => "↘",
Some("SingleDown") => "↓",
Some("DoubleDown") => "⇊",
_ => "-",
}
}
}
pub struct Client {
http: reqwest::Client,
base_url: String,
token: String,
}
impl Client {
pub fn for_site(site: &Site) -> Result<Self> {
let http = reqwest::Client::builder()
.user_agent(concat!("sugarrush/", env!("CARGO_PKG_VERSION")))
.build()
.context("failed to build HTTP client")?;
Ok(Self {
http,
base_url: site.base_url().to_string(),
token: site.token.clone(),
})
}
pub async fn entries_range(
&self,
start_ms: i64,
end_ms: i64,
want: usize,
) -> Result<Vec<Entry>> {
let count = want.max(1);
let url = format!("{}/api/v1/entries/sgv.json", self.base_url);
let entries: Vec<Entry> = self
.http
.get(&url)
.query(&[
("find[date][$gte]", start_ms.to_string()),
("find[date][$lte]", end_ms.to_string()),
("count", count.to_string()),
("token", self.token.clone()),
])
.send()
.await
.context("request to Nightscout failed")?
.error_for_status()
.context("Nightscout returned an error status")?
.json()
.await
.context("failed to parse Nightscout response")?;
Ok(entries)
}
pub async fn predictions(&self) -> Result<Option<Vec<Prediction>>> {
let url = format!("{}/api/v1/devicestatus.json", self.base_url);
let value: Value = self
.http
.get(&url)
.query(&[("count", "1"), ("token", self.token.as_str())])
.send()
.await
.context("devicestatus request failed")?
.error_for_status()
.context("Nightscout returned an error status")?
.json()
.await
.context("failed to parse devicestatus response")?;
Ok(value
.as_array()
.and_then(|items| items.first())
.and_then(parse_predicted))
}
pub async fn device_status(&self) -> Result<DeviceStatus> {
let url = format!("{}/api/v1/devicestatus.json", self.base_url);
let value: Value = self
.http
.get(&url)
.query(&[("count", "1"), ("token", self.token.as_str())])
.send()
.await
.context("devicestatus request failed")?
.error_for_status()
.context("Nightscout returned an error status")?
.json()
.await
.context("failed to parse devicestatus response")?;
Ok(value
.as_array()
.and_then(|items| items.first())
.map(parse_device_status)
.unwrap_or_default())
}
pub async fn sensor_start(&self) -> Result<Option<i64>> {
let url = format!("{}/api/v1/treatments.json", self.base_url);
let value: Value = self
.http
.get(&url)
.query(&[("count", "50"), ("token", self.token.as_str())])
.send()
.await
.context("treatments request failed")?
.error_for_status()
.context("Nightscout returned an error status")?
.json()
.await
.context("failed to parse treatments response")?;
Ok(value.as_array().and_then(|items| {
items
.iter()
.filter_map(|t| {
let event = t.get("eventType")?.as_str()?;
event
.contains("Sensor")
.then(|| {
t.get("created_at")
.and_then(Value::as_str)
.and_then(parse_iso)
})
.flatten()
})
.max()
}))
}
pub async fn treatments(&self, start_ms: i64, end_ms: i64) -> Result<Vec<Treatment>> {
let since = DateTime::from_timestamp_millis(start_ms)
.map(|dt| dt.to_rfc3339())
.unwrap_or_default();
let url = format!("{}/api/v1/treatments.json", self.base_url);
let value: Value = self
.http
.get(&url)
.query(&[
("find[created_at][$gte]", since.as_str()),
("count", "300"),
("token", self.token.as_str()),
])
.send()
.await
.context("treatments request failed")?
.error_for_status()
.context("Nightscout returned an error status")?
.json()
.await
.context("failed to parse treatments response")?;
Ok(value
.as_array()
.map(|items| {
items
.iter()
.filter_map(|t| {
let at = t.get("mills").and_then(Value::as_i64).or_else(|| {
t.get("created_at")
.and_then(Value::as_str)
.and_then(parse_iso)
})?;
if at < start_ms || at > end_ms {
return None;
}
let carbs = t.get("carbs").and_then(Value::as_f64).filter(|c| *c > 0.0);
let insulin = t
.get("insulin")
.and_then(Value::as_f64)
.filter(|i| *i > 0.0);
(carbs.is_some() || insulin.is_some()).then_some(Treatment {
at_ms: at,
carbs,
insulin,
})
})
.collect()
})
.unwrap_or_default())
}
}
#[derive(Debug, Clone)]
pub struct Treatment {
pub at_ms: i64,
pub carbs: Option<f64>,
pub insulin: Option<f64>,
}
#[derive(Debug, Clone, Default)]
pub struct DeviceStatus {
pub battery: Option<i64>,
pub device: Option<String>,
pub last_ms: Option<i64>,
pub iob: Option<f64>,
pub cob: Option<f64>,
}
fn parse_device_status(item: &Value) -> DeviceStatus {
let battery = item
.get("uploader")
.and_then(|u| u.get("battery"))
.or_else(|| item.get("uploaderBattery"))
.and_then(Value::as_i64);
let device = item
.get("device")
.and_then(Value::as_str)
.map(str::to_string);
let last_ms = item.get("mills").and_then(Value::as_i64).or_else(|| {
item.get("created_at")
.and_then(Value::as_str)
.and_then(parse_iso)
});
let l = item.get("loop");
let s = item.get("openaps").and_then(|o| o.get("suggested"));
let iob = l
.and_then(|l| l.get("iob"))
.and_then(|i| i.get("iob"))
.or_else(|| s.and_then(|s| s.get("IOB")))
.and_then(Value::as_f64);
let cob = l
.and_then(|l| l.get("cob"))
.and_then(|c| c.get("cob"))
.or_else(|| s.and_then(|s| s.get("COB")))
.and_then(Value::as_f64);
DeviceStatus {
battery,
device,
last_ms,
iob,
cob,
}
}
#[derive(Debug, Clone, Copy)]
pub struct Prediction {
pub at_ms: i64,
pub low: f64,
pub high: f64,
}
fn parse_predicted(item: &Value) -> Option<Vec<Prediction>> {
if let Some(pred) = item.get("loop").and_then(|l| l.get("predicted")) {
let start = pred
.get("startDate")
.and_then(Value::as_str)
.and_then(parse_iso)?;
let values = pred.get("values")?.as_array()?;
return Some(envelope(start, &[values]));
}
if let Some(sug) = item.get("openaps").and_then(|o| o.get("suggested")) {
let start = sug
.get("timestamp")
.and_then(Value::as_str)
.and_then(parse_iso)?;
let curves: Vec<&Vec<Value>> = sug
.get("predBGs")?
.as_object()?
.values()
.filter_map(Value::as_array)
.collect();
if curves.is_empty() {
return None;
}
return Some(envelope(start, &curves));
}
None
}
fn envelope(start_ms: i64, curves: &[&Vec<Value>]) -> Vec<Prediction> {
let max_len = curves.iter().map(|c| c.len()).max().unwrap_or(0);
let mut out = Vec::with_capacity(max_len);
for i in 0..max_len {
let mut lo = f64::MAX;
let mut hi = f64::MIN;
for c in curves {
if let Some(v) = c.get(i).and_then(Value::as_f64) {
lo = lo.min(v);
hi = hi.max(v);
}
}
if lo <= hi {
out.push(Prediction {
at_ms: start_ms + i as i64 * PRED_STEP_MS,
low: lo,
high: hi,
});
}
}
out
}
fn parse_iso(s: &str) -> Option<i64> {
DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.timestamp_millis())
}