diff options
author | Albin Suresh <albin.suresh@softwareag.com> | 2021-12-22 15:02:24 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-22 15:02:24 +0530 |
commit | 0d61811849ce9931b91b65a7e94865d99f4bb394 (patch) | |
tree | 5b6e2b4d3d44be2f1256842fb4b9d16c9e7ca2df /crates/core | |
parent | 888a4e70ca44d9b41d645148b7a24d89a984da7c (diff) |
Closes #667 Thin Edge JSON alarm support (#726)
* Closes #667 Thin Edge JSON alarm support
Diffstat (limited to 'crates/core')
-rw-r--r-- | crates/core/c8y_smartrest/Cargo.toml | 2 | ||||
-rw-r--r-- | crates/core/c8y_smartrest/src/alarm.rs | 137 | ||||
-rw-r--r-- | crates/core/c8y_smartrest/src/error.rs | 3 | ||||
-rw-r--r-- | crates/core/c8y_smartrest/src/lib.rs | 1 | ||||
-rw-r--r-- | crates/core/c8y_translator/src/json.rs | 2 | ||||
-rw-r--r-- | crates/core/tedge_mapper/src/c8y_converter.rs | 52 | ||||
-rw-r--r-- | crates/core/tedge_mapper/src/error.rs | 6 | ||||
-rw-r--r-- | crates/core/thin_edge_json/Cargo.toml | 6 | ||||
-rw-r--r-- | crates/core/thin_edge_json/src/alarm.rs | 217 | ||||
-rw-r--r-- | crates/core/thin_edge_json/src/lib.rs | 1 |
10 files changed, 413 insertions, 14 deletions
diff --git a/crates/core/c8y_smartrest/Cargo.toml b/crates/core/c8y_smartrest/Cargo.toml index 8b91157b..f98f0d34 100644 --- a/crates/core/c8y_smartrest/Cargo.toml +++ b/crates/core/c8y_smartrest/Cargo.toml @@ -5,6 +5,8 @@ authors = ["thin-edge.io team <info@thin-edge.io>"] edition = "2018" [dependencies] +thin_edge_json = { path = "../thin_edge_json" } +time = { version = "0.3", features = ["macros"] } chrono = { version = "0.4", features = ["serde"] } csv = "1.1" json_sm = { path = "../json_sm" } diff --git a/crates/core/c8y_smartrest/src/alarm.rs b/crates/core/c8y_smartrest/src/alarm.rs new file mode 100644 index 00000000..630debe9 --- /dev/null +++ b/crates/core/c8y_smartrest/src/alarm.rs @@ -0,0 +1,137 @@ +use thin_edge_json::alarm::{AlarmSeverity, ThinEdgeAlarm}; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; + +use crate::error::SmartRestSerializerError; + +/// Converts from thin-edge alarm to C8Y alarm SmartREST message +pub fn serialize_alarm(alarm: ThinEdgeAlarm) -> Result<String, SmartRestSerializerError> { + match alarm.data { + None => Ok(format!("306,{}", alarm.name)), + Some(alarm_data) => { + let smartrest_code = match alarm.severity { + AlarmSeverity::Critical => 301, + AlarmSeverity::Major => 302, + AlarmSeverity::Minor => 303, + AlarmSeverity::Warning => 304, + }; + + let current_timestamp = OffsetDateTime::now_utc(); + + let smartrest_message = format!( + "{},{},\"{}\",{}", + smartrest_code, + alarm.name, + alarm_data.message.unwrap_or_default(), + alarm_data.time.map_or_else( + || current_timestamp.format(&Rfc3339), + |timestamp| timestamp.format(&Rfc3339) + )? + ); + + Ok(smartrest_message) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use serde::Deserialize; + use test_case::test_case; + use thin_edge_json::alarm::ThinEdgeAlarmData; + use time::macros::datetime; + + #[test_case( + ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Critical, + data: Some(ThinEdgeAlarmData { + message: Some("I raised it".into()), + time: Some(datetime!(2021-04-23 19:00:00 +05:00)), + }), + }, + "301,temperature_alarm,\"I raised it\",2021-04-23T19:00:00+05:00" + ;"critical alarm translation" + )] + #[test_case( + ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Major, + data: Some(ThinEdgeAlarmData { + message: Some("I raised it".into()), + time: Some(datetime!(2021-04-23 19:00:00 +05:00)), + }), + }, + "302,temperature_alarm,\"I raised it\",2021-04-23T19:00:00+05:00" + ;"major alarm translation" + )] + #[test_case( + ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Minor, + data: Some(ThinEdgeAlarmData { + message: None, + time: Some(datetime!(2021-04-23 19:00:00 +05:00)), + }), + }, + "303,temperature_alarm,\"\",2021-04-23T19:00:00+05:00" + ;"minor alarm translation without message" + )] + #[test_case( + ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Warning, + data: Some(ThinEdgeAlarmData { + message: Some("I, raised, it".into()), + time: Some(datetime!(2021-04-23 19:00:00 +05:00)), + }), + }, + "304,temperature_alarm,\"I, raised, it\",2021-04-23T19:00:00+05:00" + ;"warning alarm translation with commas in message" + )] + #[test_case( + ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Minor, + data: None, + }, + "306,temperature_alarm" + ;"clear alarm translation" + )] + fn check_alarm_translation(alarm: ThinEdgeAlarm, expected_smartrest_msg: &str) { + let result = serialize_alarm(alarm); + + assert_eq!(result.unwrap(), expected_smartrest_msg); + } + + #[derive(Debug, Deserialize)] + struct SmartRestAlarm { + pub code: i32, + pub name: String, + pub message: Option<String>, + pub time: Option<OffsetDateTime>, + } + + #[test] + fn alarm_translation_empty_json_payload_generates_timestamp() { + let alarm = ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Warning, + data: Some(ThinEdgeAlarmData { + message: Some("I raised it".into()), + time: None, + }), + }; + + let smartrest_message = serialize_alarm(alarm).unwrap(); + let mut reader = csv::Reader::from_reader(smartrest_message.as_bytes()); + for result in reader.deserialize() { + let smartrest_alarm: SmartRestAlarm = result.unwrap(); + assert_eq!(smartrest_alarm.code, 301); + assert_eq!(smartrest_alarm.name, "empty_alarm".to_string()); + assert_eq!(smartrest_alarm.message, None); + assert_matches!(smartrest_alarm.time, Some(_)) + } + } +} diff --git a/crates/core/c8y_smartrest/src/error.rs b/crates/core/c8y_smartrest/src/error.rs index 4539ac02..d7463c1a 100644 --- a/crates/core/c8y_smartrest/src/error.rs +++ b/crates/core/c8y_smartrest/src/error.rs @@ -13,6 +13,9 @@ pub enum SmartRestSerializerError { #[error(transparent)] FromUtf8Error(#[from] std::string::FromUtf8Error), + + #[error(transparent)] + FromTimeFormatError(#[from] time::error::Format), } #[derive(thiserror::Error, Debug)] diff --git a/crates/core/c8y_smartrest/src/lib.rs b/crates/core/c8y_smartrest/src/lib.rs index 596af9be..4479f304 100644 --- a/crates/core/c8y_smartrest/src/lib.rs +++ b/crates/core/c8y_smartrest/src/lib.rs @@ -1,3 +1,4 @@ +pub mod alarm; pub mod error; pub mod smartrest_deserializer; pub mod smartrest_serializer; diff --git a/crates/core/c8y_translator/src/json.rs b/crates/core/c8y_translator/src/json.rs index 5794513f..8c9a2b5b 100644 --- a/crates/core/c8y_translator/src/json.rs +++ b/crates/core/c8y_translator/src/json.rs @@ -27,7 +27,7 @@ pub enum CumulocityJsonError { ThinEdgeJsonParserError(#[from] ThinEdgeJsonParserError), } -/// Converts from thin-edge Json to c8y_json +/// Converts from thin-edge measurement JSON to C8Y measurement JSON pub fn from_thin_edge_json(input: &str) -> Result<String, CumulocityJsonError> { let timestamp = WallClock.now(); let c8y_vec = from_thin_edge_json_with_timestamp(input, timestamp, None)?; diff --git a/crates/core/tedge_mapper/src/c8y_converter.rs b/crates/core/tedge_mapper/src/c8y_converter.rs index 23d6020f..6fb9997a 100644 --- a/crates/core/tedge_mapper/src/c8y_converter.rs +++ b/crates/core/tedge_mapper/src/c8y_converter.rs @@ -1,10 +1,12 @@ use crate::error::*; use crate::size_threshold::SizeThreshold; use crate::{converter::*, operations::Operations}; +use c8y_smartrest::alarm; use c8y_smartrest::smartrest_serializer::{SmartRestSerializer, SmartRestSetSupportedOperations}; use c8y_translator::json; use mqtt_client::{Message, Topic}; use std::collections::HashSet; +use thin_edge_json::alarm::ThinEdgeAlarm; const SMARTREST_PUBLISH_TOPIC: &str = "c8y/s/us"; @@ -19,7 +21,10 @@ impl CumulocityConverter { let mut topic_fiter = make_valid_topic_filter_or_panic("tedge/measurements"); let () = topic_fiter .add("tedge/measurements/+") - .expect("invalid topic filter"); + .expect("invalid measurement topic filter"); + let () = topic_fiter + .add("tedge/alarms/+/+") + .expect("invalid alarm topic filter"); let mapper_config = MapperConfig { in_topic_filter: topic_fiter, @@ -34,18 +39,11 @@ impl CumulocityConverter { mapper_config, } } -} - -impl Converter for CumulocityConverter { - type Error = ConversionError; - - fn get_mapper_config(&self) -> &MapperConfig { - &self.mapper_config - } - - fn try_convert(&mut self, input: &Message) -> Result<Vec<Message>, ConversionError> { - let () = self.size_threshold.validate(input.payload_str()?)?; + fn try_convert_measurement( + &mut self, + input: &Message, + ) -> Result<Vec<Message>, ConversionError> { let mut vec: Vec<Message> = Vec::new(); let maybe_child_id = get_child_id_from_topic(&input.topic.name)?; @@ -79,6 +77,36 @@ impl Converter for CumulocityConverter { Ok(vec) } + fn try_convert_alarm(&self, input: &Message) -> Result<Vec<Message>, ConversionError> { + let c8y_alarm_topic = Topic::new_unchecked(SMARTREST_PUBLISH_TOPIC); + let mut vec: Vec<Message> = Vec::new(); + + let tedge_alarm = ThinEdgeAlarm::try_from(input.topic.name.as_str(), input.payload_str()?)?; + let smartrest_alarm = alarm::serialize_alarm(tedge_alarm)?; + vec.push(Message::new(&c8y_alarm_topic, smartrest_alarm)); + + Ok(vec) + } +} + +impl Converter for CumulocityConverter { + type Error = ConversionError; + + fn get_mapper_config(&self) -> &MapperConfig { + &self.mapper_config + } + + fn try_convert(&mut self, input: &Message) -> Result<Vec<Message>, ConversionError> { + let () = self.size_threshold.validate(input.payload_str()?)?; + if input.topic.name.starts_with("tedge/measurement") { + self.try_convert_measurement(input) + } else if input.topic.name.starts_with("tedge/alarms") { + self.try_convert_alarm(input) + } else { + return Err(ConversionError::UnsupportedTopic(input.topic.name.clone())); + } + } + fn try_init_messages(&self) -> Result<Vec<Message>, ConversionError> { let ops = Operations::try_new("/etc/tedge/operations")?; let ops = ops.get_operations_list("c8y"); diff --git a/crates/core/tedge_mapper/src/error.rs b/crates/core/tedge_mapper/src/error.rs index 21ec16df..b1aa90b5 100644 --- a/crates/core/tedge_mapper/src/error.rs +++ b/crates/core/tedge_mapper/src/error.rs @@ -33,6 +33,9 @@ pub enum ConversionError { FromThinEdgeJsonSerialization(#[from] ThinEdgeJsonSerializationError), #[error(transparent)] + FromThinEdgeJsonDeserialization(#[from] thin_edge_json::alarm::ThinEdgeJsonDeserializerError), + + #[error(transparent)] FromThinEdgeJsonParser(#[from] thin_edge_json::parser::ThinEdgeJsonParserError), #[error(transparent)] @@ -49,6 +52,9 @@ pub enum ConversionError { #[error(transparent)] FromSmartRestSerializerError(#[from] c8y_smartrest::error::SmartRestSerializerError), + + #[error("Unsupported topic: {0}")] + UnsupportedTopic(String), } #[derive(Debug, thiserror::Error)] diff --git a/crates/core/thin_edge_json/Cargo.toml b/crates/core/thin_edge_json/Cargo.toml index 30ef3ff1..c55e5610 100644 --- a/crates/core/thin_edge_json/Cargo.toml +++ b/crates/core/thin_edge_json/Cargo.toml @@ -7,9 +7,11 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +time = { version = "0.3", features = ["macros"] } +clock = { path = "../../common/clock" } chrono = "0.4" json-writer = { path = "../../common/json_writer" } -serde = "1" +serde = { version = "1.0", features = ["derive"] } serde_json = "1" thiserror = "1.0" @@ -21,6 +23,8 @@ mockall = "0.10" proptest = "1.0" stats_alloc = "0.1" walkdir = "2" +assert_matches = "1.5" +test-case = "1.2" [[bench]] name = "parsing" diff --git a/crates/core/thin_edge_json/src/alarm.rs b/crates/core/thin_edge_json/src/alarm.rs new file mode 100644 index 00000000..1ace73d7 --- /dev/null +++ b/crates/core/thin_edge_json/src/alarm.rs @@ -0,0 +1,217 @@ +use std::convert::{TryFrom, TryInto}; + +use serde::Deserialize; +use time::OffsetDateTime; + +/// In-memory representation of ThinEdge JSON alarm. +#[derive(Debug, Deserialize, PartialEq)] +pub struct ThinEdgeAlarm { + pub name: String, + pub severity: AlarmSeverity, + pub data: Option<ThinEdgeAlarmData>, +} + +#[derive(Debug, Deserialize, PartialEq)] +pub enum AlarmSeverity { + Critical, + Major, + Minor, + Warning, +} + +/// In-memory representation of ThinEdge JSON alarm payload +#[derive(Debug, Deserialize, PartialEq)] +pub struct ThinEdgeAlarmData { + pub message: Option<String>, + #[serde(default)] + #[serde(deserialize_with = "clock::deserialize_iso8601_timestamp")] + pub time: Option<OffsetDateTime>, +} + +#[derive(thiserror::Error, Debug)] +pub enum ThinEdgeJsonDeserializerError { + #[error("Unsupported topic: {0}")] + UnsupportedTopic(String), + + #[error("Unsupported alarm severity in topic: {0}")] + UnsupportedAlarmSeverity(String), + + #[error(transparent)] + SerdeJsonError(#[from] serde_json::error::Error), +} + +impl TryFrom<&str> for AlarmSeverity { + type Error = ThinEdgeJsonDeserializerError; + + fn try_from(value: &str) -> Result<Self, Self::Error> { + match value { + "critical" => Ok(AlarmSeverity::Critical), + "major" => Ok(AlarmSeverity::Major), + "minor" => Ok(AlarmSeverity::Minor), + "warning" => Ok(AlarmSeverity::Warning), + invalid => Err(ThinEdgeJsonDeserializerError::UnsupportedAlarmSeverity( + invalid.into(), + ))?, + } + } +} + +impl ThinEdgeAlarm { + pub fn try_from( + mqtt_topic: &str, + mqtt_payload: &str, + ) -> Result<Self, ThinEdgeJsonDeserializerError> { + let topic_split: Vec<&str> = mqtt_topic.split('/').collect(); + if topic_split.len() == 4 { + let alarm_name = topic_split[3]; + if alarm_name.is_empty() { + return Err(ThinEdgeJsonDeserializerError::UnsupportedTopic( + mqtt_topic.into(), + )); + } + + let alarm_severity = topic_split[2]; + + let alarm_data = if mqtt_payload.is_empty() { + None + } else { + Some(serde_json::from_str(mqtt_payload)?) + }; + + Ok(Self { + name: alarm_name.into(), + severity: alarm_severity.try_into()?, + data: alarm_data, + }) + } else { + return Err(ThinEdgeJsonDeserializerError::UnsupportedTopic( + mqtt_topic.into(), + )); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use serde_json::{json, Value}; + use test_case::test_case; + use time::macros::datetime; + + #[test_case( + "tedge/alarms/critical/temperature_alarm", + json!({ + "message": "I raised it", + "time": "2021-04-23T19:00:00+05:00", + }), + ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Critical, + data: Some(ThinEdgeAlarmData { + message: Some("I raised it".into()), + time: Some(datetime!(2021-04-23 19:00:00 +05:00)), + }), + }; + "critical alarm parsing" + )] + #[test_case( + "tedge/alarms/major/temperature_alarm", + json!({ + "message": "I raised it", + }), + ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Major, + data: Some(ThinEdgeAlarmData { + message: Some("I raised it".into()), + time: None, + }), + }; + "major alarm parsing without timestamp" + )] + #[test_case( + "tedge/alarms/minor/temperature_alarm", + json!({ + "time": "2021-04-23T19:00:00+05:00", + }), + ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Minor, + data: Some(ThinEdgeAlarmData { + message: None, + time: Some(datetime!(2021-04-23 19:00:00 +05:00)), + }), + }; + "minor alarm parsing without message" + )] + #[test_case( + "tedge/alarms/warning/temperature_alarm", + json!({}), + ThinEdgeAlarm { + name: "temperature_alarm".into(), + severity: AlarmSeverity::Warning, + data: Some(ThinEdgeAlarmData { + message: None, + time: None, + }), + }; + "warning alarm parsing without message or timestamp" + )] + fn parse_thin_edge_alarm_json( + alarm_topic: &str, + alarm_payload: Value, + expected_alarm: ThinEdgeAlarm, + ) { + let alarm = + ThinEdgeAlarm::try_from(alarm_topic, alarm_payload.to_string().as_str()).unwrap(); + + assert_eq!(alarm, expected_alarm); + } + + #[test] + fn alarm_translation_empty_alarm_name() { + let result = ThinEdgeAlarm::try_from("tedge/alarms/critical/", "{}"); + + assert_matches!( + result, + Err(ThinEdgeJsonDeserializerError::UnsupportedTopic(_)) + ); + } + + #[test] + fn alarm_translation_empty_severity() { + let result = ThinEdgeAlarm::try_from("tedge/alarms//some_alarm", "{}"); + + assert_matches!( + result, + Err(ThinEdgeJsonDeserializerError::UnsupportedAlarmSeverity(_)) + ); + } + + #[test] + fn alarm_translation_empty_severity_and_name() { + let result = ThinEdgeAlarm::try_from("tedge/alarms//", "{}"); + + assert_matches!( + result, + Err(ThinEdgeJsonDeserializerError::UnsupportedTopic(_)) + ); + } + + #[test] + fn alarm_translation_invalid_severity() { + let result = ThinEdgeAlarm::try_from("tedge/alarms/invalid_severity/foo", "{}"); + + assert_matches!( + result, + Err(ThinEdgeJsonDeserializerError::UnsupportedAlarmSeverity(_)) + ); + } + + #[test] + fn alarm_translation_clear_alarm_with_empty_payload() { + let result = ThinEdgeAlarm::try_from("tedge/alarms/critical/temperature_high_alarm", ""); + assert_matches!(result.unwrap().data, None); + } +} diff --git a/crates/core/thin_edge_json/src/lib.rs b/crates/core/thin_edge_json/src/lib.rs index 56664969..441b5441 100644 --- a/crates/core/thin_edge_json/src/lib.rs +++ b/crates/core/thin_edge_json/src/lib.rs @@ -1,6 +1,7 @@ //! A library to create [ThinEdgeJson][1] from bytes of json data by validating it. //! [1]: https://github.com/thin-edge/thin-edge.io/blob/main/docs/src/architecture/thin-edge-json.md +pub mod alarm; pub mod builder; pub mod data; pub mod group; |