use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{CollectionName, Extra, Meta, Timestamp, de, impl_document, time};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DeviceStatus {
#[serde(flatten)]
pub meta: Meta,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub device: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "time::iso")]
pub created_at: Option<Timestamp>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "time::millis"
)]
pub date: Option<Timestamp>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub uploader: Option<Uploader>,
#[serde(
rename = "uploaderBattery",
default,
skip_serializing_if = "Option::is_none",
with = "de::num"
)]
pub uploader_battery: Option<f64>,
#[serde(
rename = "isCharging",
default,
skip_serializing_if = "Option::is_none",
with = "de::flag"
)]
pub is_charging: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub pump: Option<Pump>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub openaps: Option<OpenAps>,
#[serde(
rename = "loop",
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub loop_status: Option<LoopStatus>,
#[serde(
rename = "override",
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub override_status: Option<Override>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub xdripjs: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub configuration: Option<Value>,
#[serde(flatten)]
pub extra: Extra,
}
impl DeviceStatus {
#[must_use]
pub fn new(device: impl Into<String>) -> Self {
Self {
meta: Meta::default(),
device: Some(device.into()),
created_at: Some(Timestamp::now()),
date: None,
uploader: None,
uploader_battery: None,
is_charging: None,
pump: None,
openaps: None,
loop_status: None,
override_status: None,
xdripjs: None,
configuration: None,
extra: Extra::new(),
}
}
#[must_use]
pub fn time(&self) -> Option<Timestamp> {
self.created_at.or(self.date)
}
#[must_use]
pub fn uploader_battery_percent(&self) -> Option<f64> {
self.uploader
.as_ref()
.and_then(|u| u.battery)
.or(self.uploader_battery)
}
fn fill_defaults(&mut self, app: &str) {
self.device.get_or_insert_with(|| app.to_owned());
match (self.created_at, self.date) {
(None, None) => {
let now = Timestamp::now();
self.created_at = Some(now);
self.date = Some(now);
}
(Some(t), None) => self.date = Some(t),
(None, Some(t)) => self.created_at = Some(t),
(Some(_), Some(_)) => {}
}
}
}
impl_document!(
DeviceStatus,
CollectionName::DeviceStatus,
timestamp = |s| s.time(),
device = |s| s.device.as_deref()
);
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Uploader {
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub battery: Option<f64>,
#[serde(
rename = "batteryVoltage",
default,
skip_serializing_if = "Option::is_none",
with = "de::num"
)]
pub battery_voltage: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub name: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Pump {
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub clock: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub reservoir: Option<f64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub battery: Option<PumpBattery>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub status: Option<PumpStatus>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub manufacturer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub iob: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extended: Option<Value>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PumpBattery {
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub percent: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub voltage: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub status: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PumpStatus {
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub status: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::flag")]
pub bolusing: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::flag")]
pub suspended: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub timestamp: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OpenAps {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub iob: Option<Value>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub suggested: Option<ApsResult>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub enacted: Option<ApsResult>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub version: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
impl OpenAps {
#[must_use]
pub fn iob_units(&self) -> Option<f64> {
let iob = match self.iob.as_ref()? {
Value::Array(items) => items.first()?,
other => other,
};
iob.get("iob").and_then(Value::as_f64)
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ApsResult {
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub timestamp: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub bg: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub temp: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub rate: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub duration: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub units: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub reason: Option<String>,
#[serde(
rename = "eventualBG",
default,
skip_serializing_if = "Option::is_none",
with = "de::num"
)]
pub eventual_bg: Option<f64>,
#[serde(
rename = "IOB",
default,
skip_serializing_if = "Option::is_none",
with = "de::num"
)]
pub iob: Option<f64>,
#[serde(
rename = "COB",
default,
skip_serializing_if = "Option::is_none",
with = "de::num"
)]
pub cob: Option<f64>,
#[serde(
rename = "sensitivityRatio",
default,
skip_serializing_if = "Option::is_none",
with = "de::num"
)]
pub sensitivity_ratio: Option<f64>,
#[serde(
rename = "insulinReq",
default,
skip_serializing_if = "Option::is_none",
with = "de::num"
)]
pub insulin_req: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::flag")]
pub received: Option<bool>,
#[serde(
rename = "predBGs",
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub pred_bgs: Option<PredBgs>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PredBgs {
#[serde(
rename = "IOB",
default,
skip_serializing_if = "Vec::is_empty",
with = "de::nums"
)]
pub iob: Vec<f64>,
#[serde(
rename = "COB",
default,
skip_serializing_if = "Vec::is_empty",
with = "de::nums"
)]
pub cob: Vec<f64>,
#[serde(
rename = "UAM",
default,
skip_serializing_if = "Vec::is_empty",
with = "de::nums"
)]
pub uam: Vec<f64>,
#[serde(
rename = "ZT",
default,
skip_serializing_if = "Vec::is_empty",
with = "de::nums"
)]
pub zt: Vec<f64>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LoopStatus {
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub timestamp: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub iob: Option<LoopValue>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub cob: Option<LoopValue>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub predicted: Option<LoopPrediction>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enacted: Option<Value>,
#[serde(
rename = "recommendedBolus",
default,
skip_serializing_if = "Option::is_none",
with = "de::num"
)]
pub recommended_bolus: Option<f64>,
#[serde(
rename = "failureReason",
default,
skip_serializing_if = "Option::is_none",
with = "de::text"
)]
pub failure_reason: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LoopValue {
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub iob: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub cob: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub timestamp: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LoopPrediction {
#[serde(
rename = "startDate",
default,
skip_serializing_if = "Option::is_none",
with = "de::text"
)]
pub start_date: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty", with = "de::nums")]
pub values: Vec<f64>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Override {
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::flag")]
pub active: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub multiplier: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::num")]
pub duration: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub timestamp: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loop_status_is_typed_not_lost() {
let raw = serde_json::json!({
"_id": "65f1c2a8e4b0a1b2c3d4e5f6",
"device": "loop://iPhone",
"created_at": "2024-05-01T12:00:00.000Z",
"pump": {"clock": "2024-05-01T12:00:00Z", "reservoir": 104.5, "battery": {"percent": 75},
"status": {"status": "normal", "bolusing": false, "suspended": false}},
"uploader": {"battery": 58, "name": "iPhone"},
"loop": {"name": "Loop", "version": "3.4.0", "timestamp": "2024-05-01T12:00:00Z",
"iob": {"iob": 1.25, "timestamp": "2024-05-01T12:00:00Z"},
"cob": {"cob": 12, "timestamp": "2024-05-01T12:00:00Z"},
"predicted": {"startDate": "2024-05-01T12:00:00Z", "values": [110, 112, 115]},
"recommendedBolus": 0},
"override": {"name": "Workout", "active": true, "multiplier": 0.5}
});
let ds: DeviceStatus =
serde_json::from_value(raw.clone()).unwrap_or_else(|e| panic!("{e}"));
let lp = ds
.loop_status
.as_ref()
.unwrap_or_else(|| panic!("loop lost"));
assert_eq!(lp.iob.as_ref().and_then(|i| i.iob), Some(1.25));
assert_eq!(lp.predicted.as_ref().map(|p| p.values.len()), Some(3));
assert_eq!(ds.uploader_battery_percent(), Some(58.0));
assert_eq!(serde_json::to_value(&ds).ok(), Some(raw));
}
#[test]
fn openaps_iob_array_form() {
let ds: DeviceStatus = serde_json::from_value(serde_json::json!({
"created_at": "2024-05-01T12:00:00Z",
"openaps": {"iob": [{"iob": 2.1, "activity": 0.02}, {"iob": 1.9}],
"suggested": {"bg": 140, "eventualBG": 120, "predBGs": {"IOB": [140, 138], "ZT": [140]}}}
}))
.unwrap_or_else(|e| panic!("{e}"));
let aps = ds.openaps.unwrap_or_default();
assert_eq!(aps.iob_units(), Some(2.1));
assert_eq!(
aps.suggested.and_then(|s| s.pred_bgs).map(|p| p.iob),
Some(vec![140.0, 138.0])
);
}
}