use std::fmt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{Extra, Timestamp, de, time};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Property {
BgNow,
Delta,
Buckets,
Direction,
RawBg,
Upbat,
Ar2,
Iob,
Cob,
Pump,
OpenAps,
Loop,
Bwp,
Cage,
Sage,
Iage,
Bage,
Basal,
DbSize,
RuntimeState,
Custom(String),
}
impl Property {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::BgNow => "bgnow",
Self::Delta => "delta",
Self::Buckets => "buckets",
Self::Direction => "direction",
Self::RawBg => "rawbg",
Self::Upbat => "upbat",
Self::Ar2 => "ar2",
Self::Iob => "iob",
Self::Cob => "cob",
Self::Pump => "pump",
Self::OpenAps => "openaps",
Self::Loop => "loop",
Self::Bwp => "bwp",
Self::Cage => "cage",
Self::Sage => "sage",
Self::Iage => "iage",
Self::Bage => "bage",
Self::Basal => "basal",
Self::DbSize => "dbsize",
Self::RuntimeState => "runtimestate",
Self::Custom(s) => s,
}
}
}
impl fmt::Display for Property {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Properties {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub bgnow: Option<BgNow>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub delta: Option<Delta>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub buckets: Option<Vec<Bucket>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub direction: Option<DirectionProperty>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub rawbg: Option<RawBg>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub upbat: Option<Upbat>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub ar2: Option<Ar2>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub iob: Option<Iob>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub cob: Option<Cob>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub pump: Option<PumpProperty>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub openaps: Option<LoopProperty>,
#[serde(
rename = "loop",
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub loop_status: Option<LoopProperty>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub bwp: Option<Bwp>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub cage: Option<DeviceAge>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub sage: Option<SensorAge>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub iage: Option<DeviceAge>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub bage: Option<DeviceAge>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub basal: Option<Basal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub dbsize: Option<DbSize>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "de::tolerant"
)]
pub runtimestate: Option<RuntimeState>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PropertySgv {
#[serde(rename = "_id", default, with = "de::text")]
pub id: Option<String>,
#[serde(default, with = "de::num")]
pub mgdl: Option<f64>,
#[serde(default, with = "time::millis")]
pub mills: Option<Timestamp>,
#[serde(default, with = "de::text")]
pub device: Option<String>,
#[serde(default, with = "de::text")]
pub direction: Option<String>,
#[serde(default)]
pub scaled: Option<Value>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BgNow {
#[serde(default, with = "de::num")]
pub mean: Option<f64>,
#[serde(default, with = "de::num")]
pub last: Option<f64>,
#[serde(default, with = "time::millis")]
pub mills: Option<Timestamp>,
#[serde(default, deserialize_with = "de::null_as_empty")]
pub sgvs: Vec<PropertySgv>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Delta {
#[serde(default, with = "de::num")]
pub absolute: Option<f64>,
#[serde(rename = "elapsedMins", default, with = "de::num")]
pub elapsed_mins: Option<f64>,
#[serde(default, with = "de::flag")]
pub interpolated: Option<bool>,
#[serde(rename = "mean5MinsAgo", default, with = "de::num")]
pub mean_5_mins_ago: Option<f64>,
#[serde(default, with = "de::num")]
pub mgdl: Option<f64>,
#[serde(default, with = "de::num")]
pub scaled: Option<f64>,
#[serde(default, with = "de::text")]
pub display: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Bucket {
#[serde(default, with = "de::int")]
pub index: Option<i64>,
#[serde(rename = "fromMills", default, with = "time::millis")]
pub from_mills: Option<Timestamp>,
#[serde(rename = "toMills", default, with = "time::millis")]
pub to_mills: Option<Timestamp>,
#[serde(default, with = "de::num")]
pub mean: Option<f64>,
#[serde(default, with = "de::num")]
pub last: Option<f64>,
#[serde(default, with = "time::millis")]
pub mills: Option<Timestamp>,
#[serde(default, deserialize_with = "de::null_as_empty")]
pub sgvs: Vec<PropertySgv>,
#[serde(rename = "isEmpty", default, with = "de::flag")]
pub is_empty: Option<bool>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DirectionProperty {
#[serde(default, with = "de::text")]
pub value: Option<String>,
#[serde(default, with = "de::text")]
pub label: Option<String>,
#[serde(default, with = "de::text")]
pub entity: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RawBg {
#[serde(default, with = "de::num")]
pub mgdl: Option<f64>,
#[serde(rename = "noiseLabel", default, with = "de::text")]
pub noise_label: Option<String>,
#[serde(rename = "displayLine", default, with = "de::text")]
pub display_line: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Upbat {
#[serde(default, with = "de::num")]
pub level: Option<f64>,
#[serde(default, with = "de::text")]
pub display: Option<String>,
#[serde(default, with = "de::text")]
pub status: Option<String>,
#[serde(default)]
pub devices: Option<Value>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Ar2 {
#[serde(default, deserialize_with = "de::tolerant")]
pub forecast: Option<Ar2Forecast>,
#[serde(default, with = "de::int")]
pub level: Option<i64>,
#[serde(rename = "eventName", default, with = "de::text")]
pub event_name: Option<String>,
#[serde(rename = "displayLine", default, with = "de::text")]
pub display_line: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Ar2Forecast {
#[serde(default, deserialize_with = "de::null_as_empty")]
pub predicted: Vec<ForecastPoint>,
#[serde(rename = "avgLoss", default, with = "de::num")]
pub avg_loss: Option<f64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ForecastPoint {
#[serde(default, with = "time::millis")]
pub mills: Option<Timestamp>,
#[serde(default, with = "de::num")]
pub mgdl: Option<f64>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Iob {
#[serde(default, with = "de::num")]
pub iob: Option<f64>,
#[serde(default, with = "de::num")]
pub basaliob: Option<f64>,
#[serde(default, with = "de::num")]
pub activity: Option<f64>,
#[serde(default, with = "de::text")]
pub source: Option<String>,
#[serde(default, with = "de::text")]
pub device: Option<String>,
#[serde(default, with = "time::millis")]
pub mills: Option<Timestamp>,
#[serde(rename = "treatmentIob", default)]
pub treatment_iob: Option<Value>,
#[serde(rename = "lastBolus", default, deserialize_with = "de::tolerant")]
pub last_bolus: Option<super::Treatment>,
#[serde(default, with = "de::text")]
pub display: Option<String>,
#[serde(rename = "displayLine", default, with = "de::text")]
pub display_line: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Cob {
#[serde(default, with = "de::num")]
pub cob: Option<f64>,
#[serde(default, with = "de::text")]
pub source: Option<String>,
#[serde(default, with = "de::text")]
pub device: Option<String>,
#[serde(default, with = "time::millis")]
pub mills: Option<Timestamp>,
#[serde(rename = "isDecaying", default, with = "de::flag")]
pub is_decaying: Option<bool>,
#[serde(rename = "decayedBy", default, with = "time::iso")]
pub decayed_by: Option<Timestamp>,
#[serde(default, with = "de::num")]
pub carbs_hr: Option<f64>,
#[serde(default)]
pub display: Option<Value>,
#[serde(rename = "displayLine", default, with = "de::text")]
pub display_line: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PumpProperty {
#[serde(default, deserialize_with = "de::tolerant")]
pub pump: Option<super::devicestatus::Pump>,
#[serde(rename = "clockMills", default, with = "time::millis")]
pub clock_mills: Option<Timestamp>,
#[serde(default)]
pub data: Option<Value>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LoopProperty {
#[serde(default, alias = "display")]
pub status: Option<LoopStatusDisplay>,
#[serde(rename = "lastEnacted", default)]
pub last_enacted: Option<Value>,
#[serde(rename = "lastSuggested", default)]
pub last_suggested: Option<Value>,
#[serde(rename = "lastPredicted", alias = "lastPredBGs", default)]
pub last_predicted: Option<Value>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct LoopStatusDisplay {
#[serde(default, with = "de::text")]
pub symbol: Option<String>,
#[serde(default, with = "de::text")]
pub code: Option<String>,
#[serde(default, with = "de::text")]
pub label: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Bwp {
#[serde(rename = "bolusEstimate", default, with = "de::num")]
pub bolus_estimate: Option<f64>,
#[serde(default, with = "de::num")]
pub iob: Option<f64>,
#[serde(default, with = "de::num")]
pub outcome: Option<f64>,
#[serde(rename = "displayLine", default, with = "de::text")]
pub display_line: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DeviceAge {
#[serde(default, with = "de::flag")]
pub found: Option<bool>,
#[serde(default, with = "de::num")]
pub age: Option<f64>,
#[serde(default, with = "de::num")]
pub days: Option<f64>,
#[serde(default, with = "de::num")]
pub hours: Option<f64>,
#[serde(rename = "treatmentDate", default, with = "time::millis")]
pub treatment_date: Option<Timestamp>,
#[serde(default, with = "de::text")]
pub notes: Option<String>,
#[serde(default, with = "de::int")]
pub level: Option<i64>,
#[serde(default, with = "de::text")]
pub display: Option<String>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SensorAge {
#[serde(rename = "Sensor Start", default, deserialize_with = "de::tolerant")]
pub sensor_start: Option<DeviceAge>,
#[serde(rename = "Sensor Change", default, deserialize_with = "de::tolerant")]
pub sensor_change: Option<DeviceAge>,
#[serde(default, with = "de::text")]
pub min: Option<String>,
}
impl SensorAge {
#[must_use]
pub fn current(&self) -> Option<&DeviceAge> {
match self.min.as_deref() {
Some("Sensor Change") => self.sensor_change.as_ref(),
_ => self.sensor_start.as_ref().or(self.sensor_change.as_ref()),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Basal {
#[serde(default, with = "de::text")]
pub display: Option<String>,
#[serde(default, deserialize_with = "de::tolerant")]
pub current: Option<BasalCurrent>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BasalCurrent {
#[serde(default, with = "de::num")]
pub basal: Option<f64>,
#[serde(default, with = "de::num")]
pub tempbasal: Option<f64>,
#[serde(default, with = "de::num")]
pub combobolusbasal: Option<f64>,
#[serde(default, with = "de::num")]
pub totalbasal: Option<f64>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DbSize {
#[serde(default, with = "de::text")]
pub display: Option<String>,
#[serde(default, with = "de::text")]
pub status: Option<String>,
#[serde(rename = "totalDataSize", default, with = "de::num")]
pub total_data_size: Option<f64>,
#[serde(rename = "dataPercentage", default, with = "de::num")]
pub data_percentage: Option<f64>,
#[serde(flatten)]
pub extra: Extra,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct RuntimeState {
#[serde(default, with = "de::text")]
pub state: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_odd_property_does_not_fail_the_rest() {
let props: Properties = serde_json::from_value(serde_json::json!({
"iob": {"iob": 1.25, "source": "Loop", "display": "1.25", "displayLine": "IOB: 1.25U"},
"cob": {"cob": 30, "isDecaying": 1, "decayedBy": "2024-05-01T13:30:00.000Z", "display": 30},
"delta": "this plugin changed its shape",
"sage": {"Sensor Start": {"found": true, "age": 50, "display": "2d2h"}, "min": "Sensor Start"},
"future_plugin": {"x": 1}
}))
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(props.iob.and_then(|i| i.iob), Some(1.25));
assert_eq!(props.cob.as_ref().and_then(|c| c.is_decaying), Some(true));
assert!(props.delta.is_none());
assert_eq!(
props.sage.and_then(|s| s.current().and_then(|a| a.age)),
Some(50.0)
);
assert!(props.extra.contains_key("future_plugin"));
}
}