use serde::{Deserialize, Serialize};
use pamoja_power::PowerMode;
use pamoja_profile::Viz;
use pamoja_telemetry::{Event, Level};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
#[default]
Ok,
Warn,
Alarm,
}
impl Status {
pub fn worst(self, other: Status) -> Status {
self.max(other)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Trend {
Rising,
Steady,
Falling,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Reading {
pub key: String,
pub value: f32,
pub unit: String,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub band: Option<[f32; 2]>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub viz: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trend: Option<Trend>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "is_false")]
pub stat: bool,
}
impl Reading {
pub fn new(key: impl Into<String>, value: f32, unit: impl Into<String>) -> Self {
Self {
key: key.into(),
value,
unit: unit.into(),
status: Status::Ok,
band: None,
viz: None,
trend: None,
state: None,
actions: None,
stat: false,
}
}
pub fn with_status(mut self, status: Status) -> Self {
self.status = status;
self
}
pub fn with_band(mut self, low: f32, high: f32) -> Self {
self.band = Some([low, high]);
self
}
pub fn with_viz(mut self, viz: Viz) -> Self {
self.viz = Some(viz.kind().to_owned());
self
}
pub fn with_trend(mut self, trend: Trend) -> Self {
self.trend = Some(trend);
self
}
pub fn with_state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.into());
self
}
pub fn with_actions(mut self, actions: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.actions = Some(actions.into_iter().map(Into::into).collect());
self
}
pub fn as_stat(mut self) -> Self {
self.stat = true;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum EventLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl From<Level> for EventLevel {
fn from(level: Level) -> Self {
match level {
Level::Trace => EventLevel::Trace,
Level::Debug => EventLevel::Debug,
Level::Info => EventLevel::Info,
Level::Warn => EventLevel::Warn,
Level::Error => EventLevel::Error,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EventRecord {
pub level: EventLevel,
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub age_secs: Option<u64>,
}
impl EventRecord {
pub fn from_event(event: &Event, age_secs: Option<u64>) -> Self {
Self {
level: event.level.into(),
code: event.code.to_owned(),
value: event.value,
age_secs,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Active,
Saver,
Critical,
}
impl From<PowerMode> for Mode {
fn from(mode: PowerMode) -> Self {
match mode {
PowerMode::Active => Mode::Active,
PowerMode::Saver => Mode::Saver,
PowerMode::Critical => Mode::Critical,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LinkKind {
Lora,
Wifi,
Cellular,
NbIot,
Satellite,
Ethernet,
Mesh,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Link {
pub kind: LinkKind,
pub strength: u8,
pub online: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Sensor {
pub id: String,
pub reading: Reading,
#[serde(skip_serializing_if = "Option::is_none")]
pub battery: Option<f32>,
pub mode: Mode,
pub history: Vec<f32>,
pub events: Vec<EventRecord>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lat: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lon: Option<f64>,
}
impl Sensor {
pub fn new(id: impl Into<String>, reading: Reading) -> Self {
Self {
id: id.into(),
reading,
battery: None,
mode: Mode::Active,
history: Vec::new(),
events: Vec::new(),
peer: None,
lat: None,
lon: None,
}
}
pub fn on_peer(mut self, peer: impl Into<String>) -> Self {
self.peer = Some(peer.into());
self
}
pub fn at(mut self, lat: f64, lon: f64) -> Self {
self.lat = Some(lat);
self.lon = Some(lon);
self
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Group {
pub id: String,
pub name: String,
pub link: Link,
pub status: Status,
pub sensors: Vec<Sensor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lat: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lon: Option<f64>,
}
impl Group {
pub fn at(mut self, lat: f64, lon: f64) -> Self {
self.lat = Some(lat);
self.lon = Some(lon);
self
}
pub fn recompute_status(&mut self) -> Status {
let mut overall = if self.link.online {
Status::Ok
} else {
Status::Warn
};
for sensor in &self.sensors {
overall = overall.worst(sensor.reading.status);
for event in &sensor.events {
overall = overall.worst(match event.level {
EventLevel::Error => Status::Alarm,
EventLevel::Warn => Status::Warn,
_ => Status::Ok,
});
}
}
self.status = overall;
overall
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Org {
pub id: String,
pub name: String,
pub groups: Vec<Group>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct State {
pub orgs: Vec<Org>,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub uptime_secs: Option<u64>,
#[serde(default, skip_serializing_if = "is_false")]
pub demo: bool,
}
fn is_false(value: &bool) -> bool {
!*value
}
impl State {
pub fn recompute_status(&mut self) -> Status {
let mut overall = Status::Ok;
for org in &mut self.orgs {
for group in &mut org.groups {
overall = overall.worst(group.recompute_status());
}
}
self.status = overall;
overall
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sensor(key: &str, value: f32, status: Status) -> Sensor {
Sensor {
id: key.to_owned(),
reading: Reading::new(key, value, "celsius")
.with_status(status)
.with_band(2.0, 8.0),
battery: Some(0.8),
mode: Mode::Active,
history: vec![value],
events: Vec::new(),
peer: None,
lat: None,
lon: None,
}
}
fn fleet(sensor_status: Status, online: bool) -> State {
State {
orgs: vec![Org {
id: "org-1".to_owned(),
name: "Org One".to_owned(),
groups: vec![Group {
id: "g1".to_owned(),
name: "Group One".to_owned(),
link: Link {
kind: LinkKind::Lora,
strength: 3,
online,
},
status: Status::Ok,
sensors: vec![sensor("temperature", 5.0, sensor_status)],
lat: None,
lon: None,
}],
}],
status: Status::Ok,
uptime_secs: Some(3600),
demo: false,
}
}
#[test]
fn status_worst_picks_the_most_urgent() {
assert_eq!(Status::Ok.worst(Status::Warn), Status::Warn);
assert_eq!(Status::Warn.worst(Status::Alarm), Status::Alarm);
}
#[test]
fn recompute_rolls_sensor_status_up_to_group_and_fleet() {
let mut state = fleet(Status::Alarm, true);
assert_eq!(state.recompute_status(), Status::Alarm);
assert_eq!(state.orgs[0].groups[0].status, Status::Alarm);
assert_eq!(state.status, Status::Alarm);
}
#[test]
fn an_offline_group_is_at_least_a_warning() {
let mut state = fleet(Status::Ok, false);
assert_eq!(state.recompute_status(), Status::Warn);
}
#[test]
fn the_fleet_round_trips_through_json() {
let mut state = fleet(Status::Warn, true);
state.recompute_status();
let json = state.to_json().expect("serialize");
let restored: State = serde_json::from_str(&json).expect("deserialize");
assert_eq!(state, restored);
}
#[test]
fn the_wire_uses_stable_lowercase_tags() {
assert_eq!(serde_json::to_string(&Status::Alarm).unwrap(), "\"alarm\"");
assert_eq!(serde_json::to_string(&LinkKind::Lora).unwrap(), "\"lora\"");
assert_eq!(serde_json::to_string(&Mode::Saver).unwrap(), "\"saver\"");
}
}