diff --git a/crates/extensions/c8y_mapper_ext/src/converter.rs b/crates/extensions/c8y_mapper_ext/src/converter.rs index af9ac6c0e33..5da4b9d8c51 100644 --- a/crates/extensions/c8y_mapper_ext/src/converter.rs +++ b/crates/extensions/c8y_mapper_ext/src/converter.rs @@ -1624,6 +1624,235 @@ pub(crate) mod tests { ); } + #[tokio::test] + async fn convert_measurement_with_nested_child_device() { + let tmp_dir = TempTedgeDir::new(); + let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; + let reg_message = Message::new( + &Topic::new_unchecked("te/device/immediate_child//"), + json!({ + "@type":"child-device", + "@parent":"device/main//", + "@id":"immediate_child" + }) + .to_string(), + ); + let _ = converter.convert(®_message).await; + + let reg_message = Message::new( + &Topic::new_unchecked("te/device/nested_child//"), + json!({ + "@type":"child-device", + "@parent":"device/immediate_child//", + "@id":"nested_child" + }) + .to_string(), + ); + let _ = converter.convert(®_message).await; + + let in_topic = "te/device/nested_child///m/"; + let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00"}"#; + let in_message = Message::new(&Topic::new_unchecked(in_topic), in_payload); + + let expected_c8y_json_message = Message::new( + &Topic::new_unchecked("c8y/measurement/measurements/create"), + json!({ + "externalSource":{"externalId":"nested_child","type":"c8y_Serial"}, + "temp":{"temp":{"value":1.0}}, + "time":"2021-11-16T17:45:40.571760714+01:00", + "type":"ThinEdgeMeasurement" + }) + .to_string(), + ); + + // Test the first output messages contains SmartREST and C8Y JSON. + let out_first_messages = converter.convert(&in_message).await; + assert_eq!(out_first_messages, vec![expected_c8y_json_message.clone()]); + } + + #[tokio::test] + async fn convert_measurement_with_nested_child_service() { + let tmp_dir = TempTedgeDir::new(); + let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; + let reg_message = Message::new( + &Topic::new_unchecked("te/device/immediate_child//"), + json!({ + "@type":"child-device", + "@parent":"device/main//", + "@id":"immediate_child" + }) + .to_string(), + ); + let _ = converter.convert(®_message).await; + + let reg_message = Message::new( + &Topic::new_unchecked("te/device/nested_child//"), + json!({ + "@type":"child-device", + "@parent":"device/immediate_child//", + "@id":"nested_child" + }) + .to_string(), + ); + let _ = converter.convert(®_message).await; + + let reg_message = Message::new( + &Topic::new_unchecked("te/device/nested_child/service/nested_service"), + json!({ + "@type":"service", + "@parent":"device/nested_child//", + "@id":"nested_service" + }) + .to_string(), + ); + let _ = converter.convert(®_message).await; + + let in_topic = "te/device/nested_child/service/nested_service/m/"; + let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00"}"#; + let in_message = Message::new(&Topic::new_unchecked(in_topic), in_payload); + + let expected_c8y_json_message = Message::new( + &Topic::new_unchecked("c8y/measurement/measurements/create"), + json!({ + "externalSource":{"externalId":"nested_service","type":"c8y_Serial"}, + "temp":{"temp":{"value":1.0}}, + "time":"2021-11-16T17:45:40.571760714+01:00", + "type":"ThinEdgeMeasurement" + }) + .to_string(), + ); + + // Test the first output messages contains SmartREST and C8Y JSON. + let out_first_messages = converter.convert(&in_message).await; + assert_eq!(out_first_messages, vec![expected_c8y_json_message.clone()]); + } + + #[tokio::test] + async fn convert_measurement_for_child_device_service() { + let tmp_dir = TempTedgeDir::new(); + let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; + + let in_topic = "te/device/child1/service/app1/m/m_type"; + let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00"}"#; + let in_message = Message::new(&Topic::new_unchecked(in_topic), in_payload); + + let expected_child_create_msg = Message::new( + &Topic::new_unchecked("te/device/child1//"), + json!({ + "@id":"test-device:device:child1", + "@type":"child-device", + "name":"child1", + }) + .to_string(), + ) + .with_retain(); + + let expected_smart_rest_message_child = Message::new( + &Topic::new_unchecked("c8y/s/us"), + "101,test-device:device:child1,child1,thin-edge.io-child", + ); + let expected_service_create_msg = Message::new( + &Topic::new_unchecked("te/device/child1/service/app1"), + json!({ + "@id":"test-device:device:child1:service:app1", + "@parent":"device/child1//", + "@type":"service", + "name":"app1", + "type":"service" + }) + .to_string(), + ) + .with_retain(); + + let expected_smart_rest_message_service = Message::new( + &Topic::new_unchecked("c8y/s/us/test-device:device:child1"), + "102,test-device:device:child1:service:app1,service,app1,up", + ); + let expected_c8y_json_message = Message::new( + &Topic::new_unchecked("c8y/measurement/measurements/create"), + json!({ + "externalSource":{ + "externalId":"test-device:device:child1:service:app1", + "type":"c8y_Serial" + }, + "temp":{"temp":{"value":1.0}}, + "time":"2021-11-16T17:45:40.571760714+01:00", + "type":"m_type"}) + .to_string(), + ); + + // Test the first output messages contains SmartREST and C8Y JSON. + let out_first_messages = converter.convert(&in_message).await; + assert_eq!( + out_first_messages, + vec![ + expected_child_create_msg, + expected_smart_rest_message_child, + expected_service_create_msg, + expected_smart_rest_message_service, + expected_c8y_json_message.clone() + ] + ); + + // Test the second output messages doesn't contain SmartREST child device creation. + let out_second_messages = converter.convert(&in_message).await; + assert_eq!(out_second_messages, vec![expected_c8y_json_message]); + } + + #[tokio::test] + async fn convert_measurement_for_main_device_service() { + let tmp_dir = TempTedgeDir::new(); + let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; + + let in_topic = "te/device/main/service/appm/m/m_type"; + let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00"}"#; + let in_message = Message::new(&Topic::new_unchecked(in_topic), in_payload); + + let expected_create_service_msg = Message::new( + &Topic::new_unchecked("te/device/main/service/appm"), + json!({ + "@id":"test-device:device:main:service:appm", + "@parent":"device/main//", + "@type":"service", + "name":"appm", + "type":"service"}) + .to_string(), + ) + .with_retain(); + + let expected_c8y_json_message = Message::new( + &Topic::new_unchecked("c8y/measurement/measurements/create"), + json!({ + "externalSource":{ + "externalId":"test-device:device:main:service:appm", + "type":"c8y_Serial" + }, + "temp":{"temp":{"value":1.0}}, + "time":"2021-11-16T17:45:40.571760714+01:00", + "type":"m_type"}) + .to_string(), + ); + + let expected_smart_rest_message_service = Message::new( + &Topic::new_unchecked("c8y/s/us"), + "102,test-device:device:main:service:appm,service,appm,up", + ); + + // Test the first output messages contains SmartREST and C8Y JSON. + let out_first_messages = converter.convert(&in_message).await; + assert_eq!( + out_first_messages, + vec![ + expected_create_service_msg, + expected_smart_rest_message_service, + expected_c8y_json_message.clone() + ] + ); + + let out_second_messages = converter.convert(&in_message).await; + assert_eq!(out_second_messages, vec![expected_c8y_json_message]); + } + #[tokio::test] #[ignore = "FIXME: the registration is currently done even if the message is ill-formed"] async fn convert_first_measurement_invalid_then_valid_with_child_id() { diff --git a/crates/extensions/c8y_mapper_ext/src/serializer.rs b/crates/extensions/c8y_mapper_ext/src/serializer.rs index c974b7b8b68..018b620dee6 100644 --- a/crates/extensions/c8y_mapper_ext/src/serializer.rs +++ b/crates/extensions/c8y_mapper_ext/src/serializer.rs @@ -58,16 +58,16 @@ impl C8yJsonSerializer { json.write_open_obj(); - if entity.r#type == EntityType::ChildDevice { - let child_id = &entity.external_id; - // In case the measurement is addressed to a child-device use fragment - // "externalSource" to tell c8Y identity API to use child-device + if entity.r#type == EntityType::ChildDevice || entity.r#type == EntityType::Service { + let entity_id = &entity.external_id; + // In case the measurement is addressed to a child-device or a service, use fragment + // "externalSource" to tell c8Y identity API to use child-device or for service // object referenced by "externalId", instead of root device object // referenced by MQTT client's Device ID. let _ = json.write_key("externalSource"); json.write_open_obj(); let _ = json.write_key("externalId"); - let _ = json.write_str(child_id.as_ref()); + let _ = json.write_str(entity_id.as_ref()); let _ = json.write_key("type"); let _ = json.write_str("c8y_Serial"); json.write_close_obj(); diff --git a/crates/extensions/c8y_mapper_ext/src/tests.rs b/crates/extensions/c8y_mapper_ext/src/tests.rs index ca0bb9cbc70..63ee61adb84 100644 --- a/crates/extensions/c8y_mapper_ext/src/tests.rs +++ b/crates/extensions/c8y_mapper_ext/src/tests.rs @@ -932,6 +932,348 @@ async fn c8y_mapper_alarm_empty_json_payload() { .starts_with(r#"303,empty_temperature_alarm,"empty_temperature_alarm""#)); } +#[tokio::test] +async fn c8y_mapper_child_event() { + let cfg_dir = TempTedgeDir::new(); + let (mqtt, _http, _fs, mut timer, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + timer.send(Timeout::new(())).await.unwrap(); //Complete sync phase so that alarm mapping starts + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + skip_init_messages(&mut mqtt).await; + + mqtt.send(MqttMessage::new( + &"te/device/external_sensor///e/custom_event" + .try_into() + .unwrap(), + json!({ + "text": "Someone logged-in", + "time":"2023-01-25T18:41:14.776170774Z", + }) + .to_string(), + )) + .await + .unwrap(); + + // Expect auto-registration message + assert_received_includes_json( + &mut mqtt, + [( + "te/device/external_sensor//", + json!({"@type":"child-device","@id":"test-device:device:external_sensor"}), + )], + ) + .await; + + // Expect child device creation message + assert_received_contains_str( + &mut mqtt, + [( + "c8y/s/us", + "101,test-device:device:external_sensor,external_sensor,thin-edge.io-child", + )], + ) + .await; + + // Expect converted temperature alarm message + assert_received_includes_json( + &mut mqtt, + [( + "c8y/event/events/create", + json!({ + "type":"custom_event", + "time":"2023-01-25T18:41:14.776170774Z", + "text":"Someone logged-in", + "externalSource": { + "externalId":"test-device:device:external_sensor", + "type":"c8y_Serial" + } + }), + )], + ) + .await; +} + +#[tokio::test] +async fn c8y_mapper_child_service_event() { + let cfg_dir = TempTedgeDir::new(); + let (mqtt, _http, _fs, mut timer, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + timer.send(Timeout::new(())).await.unwrap(); //Complete sync phase so that alarm mapping starts + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + skip_init_messages(&mut mqtt).await; + + mqtt.send(MqttMessage::new( + &"te/device/external_sensor/service/service_child/e/custom_event" + .try_into() + .unwrap(), + json!({ + "text": "Someone logged-in", + "time":"2023-01-25T18:41:14.776170774Z", + }) + .to_string(), + )) + .await + .unwrap(); + + // Expect auto-registration message + assert_received_includes_json( + &mut mqtt, + [( + "te/device/external_sensor//", + json!({"@type":"child-device","@id":"test-device:device:external_sensor"}), + )], + ) + .await; + + // Expect child device creation message + assert_received_contains_str( + &mut mqtt, + [("c8y/s/us", "101,test-device:device:external_sensor")], + ) + .await; + + // Expect child device service auto registration message + assert_received_includes_json( + &mut mqtt, + [( + "te/device/external_sensor/service/service_child", + json!({ + "@id":"test-device:device:external_sensor:service:service_child", + "@parent":"device/external_sensor//", + "@type":"service", + "name":"service_child", + "type":"service" + }), + )], + ) + .await; + + // Expect child device service creation message + assert_received_contains_str( + &mut mqtt, + [( + "c8y/s/us/test-device:device:external_sensor", + "102,test-device:device:external_sensor:service:service_child,service,service_child,up", + )], + ) + .await; + + // Expect converted event message + assert_received_includes_json( + &mut mqtt, + [( + "c8y/event/events/create", + json!({ + "type":"custom_event", + "time":"2023-01-25T18:41:14.776170774Z", + "text":"Someone logged-in", + "externalSource": { + "externalId":"test-device:device:external_sensor:service:service_child", + "type":"c8y_Serial" + } + }), + )], + ) + .await; +} + +#[tokio::test] +async fn c8y_mapper_main_service_event() { + let cfg_dir = TempTedgeDir::new(); + let (mqtt, _http, _fs, mut timer, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + timer.send(Timeout::new(())).await.unwrap(); //Complete sync phase so that alarm mapping starts + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + skip_init_messages(&mut mqtt).await; + + mqtt.send(MqttMessage::new( + &"te/device/main/service/service_main/e/custom_event" + .try_into() + .unwrap(), + json!({ + "text": "Someone logged-in", + "time":"2023-01-25T18:41:14.776170774Z", + }) + .to_string(), + )) + .await + .unwrap(); + + // Expect auto-registration message + assert_received_includes_json( + &mut mqtt, + [( + "te/device/main/service/service_main", + json!({ + "@type":"service", + "@parent":"device/main//", + "@id":"test-device:device:main:service:service_main", + "type":"service" + }), + )], + ) + .await; + + // Create the service for the main device + assert_received_contains_str( + &mut mqtt, + [( + "c8y/s/us", + "102,test-device:device:main:service:service_main,service,service_main,up", + )], + ) + .await; + + // Expect converted event for the main device service + assert_received_includes_json( + &mut mqtt, + [( + "c8y/event/events/create", + json!({ + "type":"custom_event", + "time":"2023-01-25T18:41:14.776170774Z", + "text":"Someone logged-in", + "externalSource": { + "externalId":"test-device:device:main:service:service_main", + "type":"c8y_Serial" + } + }), + )], + ) + .await; +} + +#[tokio::test] +async fn c8y_mapper_child_service_alarm() { + let cfg_dir = TempTedgeDir::new(); + let (mqtt, _http, _fs, mut timer, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + timer.send(Timeout::new(())).await.unwrap(); //Complete sync phase so that alarm mapping starts + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + skip_init_messages(&mut mqtt).await; + + mqtt.send(MqttMessage::new( + &"te/device/external_sensor/service/service_child/a/custom_alarm" + .try_into() + .unwrap(), + json!({ + "severity":"critical", + "text": "temperature alarm", + "time":"2023-01-25T18:41:14.776170774Z", + }) + .to_string(), + )) + .await + .unwrap(); + + // Expect auto-registration message + assert_received_includes_json( + &mut mqtt, + [( + "te/device/external_sensor//", + json!({"@type":"child-device","@id":"test-device:device:external_sensor"}), + )], + ) + .await; + + // Expect child device creation message + assert_received_contains_str( + &mut mqtt, + [("c8y/s/us", "101,test-device:device:external_sensor")], + ) + .await; + + // Expect child device service auto registration message + assert_received_includes_json( + &mut mqtt, + [( + "te/device/external_sensor/service/service_child", + json!({ + "@id":"test-device:device:external_sensor:service:service_child", + "@parent":"device/external_sensor//", + "@type":"service", + "name":"service_child", + "type":"service" + }), + )], + ) + .await; + + // Expect child device service creation message + assert_received_contains_str( + &mut mqtt, + [( + "c8y/s/us/test-device:device:external_sensor", + "102,test-device:device:external_sensor:service:service_child,service,service_child,up", + )], + ) + .await; + + // Expect converted alarm for the main device service + assert_received_contains_str( + &mut mqtt, + [( + "c8y/s/us/test-device:device:external_sensor:service:service_child", + r#"301,custom_alarm,"temperature alarm",2023-01-25T18:41:14.776170774Z"#, + )], + ) + .await; +} + +#[tokio::test] +async fn c8y_mapper_main_service_alarm() { + let cfg_dir = TempTedgeDir::new(); + let (mqtt, _http, _fs, mut timer, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + timer.send(Timeout::new(())).await.unwrap(); //Complete sync phase so that alarm mapping starts + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + skip_init_messages(&mut mqtt).await; + + mqtt.send(MqttMessage::new( + &"te/device/main/service/service_main/a/custom_alarm" + .try_into() + .unwrap(), + json!({ + "severity":"critical", + "text": "temperature alarm", + "time":"2023-01-25T18:41:14.776170774Z", + }) + .to_string(), + )) + .await + .unwrap(); + + // Expect auto-registration message + assert_received_includes_json( + &mut mqtt, + [( + "te/device/main/service/service_main", + json!({ + "@type":"service", + "@parent":"device/main//", + "@id":"test-device:device:main:service:service_main", + "type":"service" + }), + )], + ) + .await; + + // Create the service for the main device + assert_received_contains_str( + &mut mqtt, + [( + "c8y/s/us", + "102,test-device:device:main:service:service_main,service,service_main,up", + )], + ) + .await; + + // Expect converted alarm for the main device service + assert_received_contains_str( + &mut mqtt, + [( + "c8y/s/us/test-device:device:main:service:service_main", + r#"301,custom_alarm,"temperature alarm",2023-01-25T18:41:14.776170774Z"#, + )], + ) + .await; +} + #[tokio::test] async fn c8y_mapper_alarm_complex_text_fragment_in_payload_failed() { let cfg_dir = TempTedgeDir::new(); @@ -2288,6 +2630,264 @@ async fn handle_log_upload_successful_cmd_for_child_device() { .await; } +#[tokio::test] +async fn c8y_mapper_nested_child_alarm_mapping_to_smartrest() { + let cfg_dir = TempTedgeDir::new(); + let (mqtt, _http, _fs, mut timer, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + + timer.send(Timeout::new(())).await.unwrap(); //Complete sync phase so that alarm mapping starts + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + skip_init_messages(&mut mqtt).await; + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/immediate_child//"), + json!({ + "@type":"child-device", + "@parent":"device/main//", + "@id":"immediate_child" + }) + .to_string(), + ); + mqtt.send(reg_message).await.unwrap(); + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child//"), + json!({ + "@type":"child-device", + "@parent":"device/immediate_child//", + "@id":"nested_child" + }) + .to_string(), + ); + mqtt.send(reg_message).await.unwrap(); + + mqtt.send(MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child///a/"), + json!({ "severity": "minor", "text": "Temperature high","time":"2023-10-13T15:00:07.172674353Z" }).to_string(), + )) + .await + .unwrap(); + + // Expect nested child device creating an minor alarm + assert_received_contains_str( + &mut mqtt, + [ + ( + "c8y/s/us", + "101,immediate_child,immediate_child,thin-edge.io-child", + ), + ( + "c8y/s/us/immediate_child", + "101,nested_child,nested_child,thin-edge.io-child", + ), + ( + "c8y/s/us/nested_child", + "303,ThinEdgeAlarm,\"Temperature high\",2023-10-13T15:00:07.172674353Z", + ), + ], + ) + .await; +} + +#[tokio::test] +async fn c8y_mapper_nested_child_event_mapping_to_smartrest() { + let cfg_dir = TempTedgeDir::new(); + let (mqtt, _http, _fs, mut timer, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + + timer.send(Timeout::new(())).await.unwrap(); //Complete sync phase so that alarm mapping starts + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + skip_init_messages(&mut mqtt).await; + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/immediate_child//"), + json!({ + "@type":"child-device", + "@parent":"device/main//", + "@id":"immediate_child" + }) + .to_string(), + ); + mqtt.send(reg_message).await.unwrap(); + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child//"), + json!({ + "@type":"child-device", + "@parent":"device/immediate_child//", + "@id":"nested_child" + }) + .to_string(), + ); + mqtt.send(reg_message).await.unwrap(); + + mqtt.send(MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child///e/"), + json!({ "text": "Temperature high","time":"2023-10-13T15:00:07.172674353Z" }).to_string(), + )) + .await + .unwrap(); + + assert_received_contains_str( + &mut mqtt, + [ + ( + "c8y/s/us", + "101,immediate_child,immediate_child,thin-edge.io-child", + ), + ( + "c8y/s/us/immediate_child", + "101,nested_child,nested_child,thin-edge.io-child", + ), + ], + ) + .await; + // Expect nested child device creating an event + assert_received_includes_json( + &mut mqtt, + [( + "c8y/event/events/create", + json!({ + "type":"ThinEdgeEvent", + "time":"2023-10-13T15:00:07.172674353Z", + "text":"Temperature high", + "externalSource":{"externalId":"nested_child","type":"c8y_Serial"} + }), + )], + ) + .await; +} + +#[tokio::test] +async fn c8y_mapper_nested_child_service_alarm_mapping_to_smartrest() { + let cfg_dir = TempTedgeDir::new(); + let (mqtt, _http, _fs, mut timer, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + + timer.send(Timeout::new(())).await.unwrap(); //Complete sync phase so that alarm mapping starts + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + skip_init_messages(&mut mqtt).await; + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/immediate_child//"), + json!({ + "@type":"child-device", + "@parent":"device/main//", + "@id":"immediate_child" + }) + .to_string(), + ); + mqtt.send(reg_message).await.unwrap(); + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child//"), + json!({ + "@type":"child-device", + "@parent":"device/immediate_child//", + "@id":"nested_child" + }) + .to_string(), + ); + mqtt.send(reg_message).await.unwrap(); + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child/service/nested_service"), + json!({ + "@type":"service", + "@parent":"device/nested_child//", + "@id":"nested_service" + }) + .to_string(), + ); + + mqtt.send(reg_message).await.unwrap(); + + mqtt.send(MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child/service/nested_service/a/"), + json!({ "severity": "minor", "text": "Temperature high","time":"2023-10-13T15:00:07.172674353Z" }).to_string(), + )) + .await + .unwrap(); + + mqtt.skip(3).await; + + // Expect child device service creating minor temperature alarm messages + assert_received_contains_str( + &mut mqtt, + [( + "c8y/s/us/nested_service", + "303,ThinEdgeAlarm,\"Temperature high\",2023-10-13T15:00:07.172674353Z", + )], + ) + .await; +} + +#[tokio::test] +async fn c8y_mapper_nested_child_service_event_mapping_to_smartrest() { + let cfg_dir = TempTedgeDir::new(); + let (mqtt, _http, _fs, mut timer, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + + timer.send(Timeout::new(())).await.unwrap(); //Complete sync phase so that alarm mapping starts + let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); + skip_init_messages(&mut mqtt).await; + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/immediate_child//"), + json!({ + "@type":"child-device", + "@parent":"device/main//", + "@id":"immediate_child" + }) + .to_string(), + ); + mqtt.send(reg_message).await.unwrap(); + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child//"), + json!({ + "@type":"child-device", + "@parent":"device/immediate_child//", + "@id":"nested_child" + }) + .to_string(), + ); + mqtt.send(reg_message).await.unwrap(); + + let reg_message = MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child/service/nested_service"), + json!({ + "@type":"service", + "@parent":"device/nested_child//", + "@id":"nested_service" + }) + .to_string(), + ); + + mqtt.send(reg_message).await.unwrap(); + + mqtt.send(MqttMessage::new( + &Topic::new_unchecked("te/device/nested_child/service/nested_service/e/"), + json!({ "text": "Temperature high","time":"2023-10-13T15:00:07.172674353Z" }).to_string(), + )) + .await + .unwrap(); + + mqtt.skip(3).await; + + // Expect nested child device service creating the event messages + assert_received_includes_json( + &mut mqtt, + [( + "c8y/event/events/create", + json!({ + "type":"ThinEdgeEvent", + "time":"2023-10-13T15:00:07.172674353Z", + "text":"Temperature high", + "externalSource":{"externalId":"nested_service","type":"c8y_Serial"} + }), + )], + ) + .await; +} + fn assert_command_exec_log_content(cfg_dir: TempTedgeDir, expected_contents: &str) { let paths = fs::read_dir(cfg_dir.to_path_buf().join("tedge/agent")).unwrap(); for path in paths { diff --git a/tests/RobotFramework/tests/cumulocity/telemetry/child_device_telemetry.robot b/tests/RobotFramework/tests/cumulocity/telemetry/child_device_telemetry.robot index b8f7d7e9bb7..3830a70653b 100644 --- a/tests/RobotFramework/tests/cumulocity/telemetry/child_device_telemetry.robot +++ b/tests/RobotFramework/tests/cumulocity/telemetry/child_device_telemetry.robot @@ -13,8 +13,6 @@ Child devices support sending simple measurements ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=ThinEdgeMeasurement value=temperature series=temperature Log ${measurements} - - Child devices support sending simple measurements with custom type in topic Execute Command tedge mqtt pub te/device/${CHILD_SN}///m/CustomType_topic '{ "temperature": 25 }' ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=CustomType_topic value=temperature series=temperature @@ -121,6 +119,129 @@ Nested child devices support sending inventory data via tedge topic Should Be Equal ${mo["name"]} ${nested_child} +# +# Services +# +# measurements +Send measurements to an unregistered child service + Execute Command tedge mqtt pub te/device/${CHILD_SN}/service/app1/m/m_type '{"temperature": 30.1}' + Cumulocity.Device Should Exist ${CHILD_SN} + Cumulocity.Should Have Services min_count=1 max_count=1 name=app1 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:${CHILD_SN}:service:app1 + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=m_type + Should Be Equal ${measurements[0]["type"]} m_type + Should Be Equal As Numbers ${measurements[0]["temperature"]["temperature"]["value"]} 30.1 + +Send measurements to a registered child service + Execute Command tedge mqtt pub --retain te/device/${CHILD_SN}/service/app2 '{"@type":"service","@parent":"device/${CHILD_SN}//"}' + Cumulocity.Device Should Exist ${CHILD_SN} + Cumulocity.Should Have Services name=app2 min_count=1 max_count=1 + + Execute Command tedge mqtt pub te/device/${CHILD_SN}/service/app2/m/m_type '{"temperature": 30.1}' + Cumulocity.Device Should Exist ${DEVICE_SN}:device:${CHILD_SN}:service:app2 + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=m_type + Should Be Equal ${measurements[0]["type"]} m_type + Should Be Equal As Numbers ${measurements[0]["temperature"]["temperature"]["value"]} 30.1 + +# alarms +Send alarms to an unregistered child service + Execute Command tedge mqtt pub te/device/${CHILD_SN}/service/app3/a/alarm_001 '{"text": "test alarm","severity":"major"}' + Cumulocity.Device Should Exist ${CHILD_SN} + Cumulocity.Should Have Services min_count=1 max_count=1 name=app3 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:${CHILD_SN}:service:app3 + ${alarms}= Device Should Have Alarm/s expected_text=test alarm type=alarm_001 minimum=1 maximum=1 + Should Be Equal ${alarms[0]["type"]} alarm_001 + Should Be Equal ${alarms[0]["severity"]} MAJOR + +Send alarms to a registered child service + Execute Command tedge mqtt pub --retain te/device/${CHILD_SN}/service/app4 '{"@type":"service","@parent":"device/${CHILD_SN}//"}' + Cumulocity.Device Should Exist ${CHILD_SN} + Cumulocity.Should Have Services name=app4 min_count=1 max_count=1 + + Execute Command tedge mqtt pub te/device/${CHILD_SN}/service/app4/a/alarm_002 '{"text": "test alarm"}' + Cumulocity.Device Should Exist ${DEVICE_SN}:device:${CHILD_SN}:service:app4 + ${alarms}= Device Should Have Alarm/s expected_text=test alarm type=alarm_002 minimum=1 maximum=1 + Should Be Equal ${alarms[0]["type"]} alarm_002 + +# events +Send events to an unregistered child service + Execute Command tedge mqtt pub te/device/${CHILD_SN}/service/app5/e/event_001 '{"text": "test event"}' + Cumulocity.Device Should Exist ${CHILD_SN} + Cumulocity.Should Have Services name=app5 min_count=1 max_count=1 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:${CHILD_SN}:service:app5 + Device Should Have Event/s expected_text=test event type=event_001 minimum=1 maximum=1 + +Send events to a registered child service + Execute Command tedge mqtt pub --retain te/device/${CHILD_SN}/service/app6 '{"@type":"service","@parent":"device/${CHILD_SN}//"}' + Cumulocity.Device Should Exist ${CHILD_SN} + Cumulocity.Should Have Services name=app6 min_count=1 max_count=1 + Cumulocity.Device Should Exist ${DEVICE_SN}:device:${CHILD_SN}:service:app6 + Execute Command tedge mqtt pub te/device/${CHILD_SN}/service/app6/e/event_002 '{"text": "test event"}' + Device Should Have Event/s expected_text=test event type=event_002 minimum=1 maximum=1 + +# Nested child devices +Nested child devices support sending measurement + ${nested_child}= Get Random Name + Execute Command tedge mqtt pub --retain 'te/device/${nested_child}//' '{"@type":"child-device","@parent":"device/${CHILD_SN}//","@id":"${nested_child}"}' + Execute Command tedge mqtt pub te/device/${nested_child}///m/ '{ "temperature": 25 }' + Cumulocity.Device Should Exist ${nested_child} + ${measurements}= Device Should Have Measurements type=ThinEdgeMeasurement value=temperature series=temperature minimum=1 maximum=1 + Log ${measurements} + + +Nested child devices support sending alarm + ${nested_child}= Get Random Name + Execute Command tedge mqtt pub --retain 'te/device/${nested_child}//' '{"@type":"child-device","@parent":"device/${CHILD_SN}//","@id":"${nested_child}"}' + Execute Command tedge mqtt pub te/device/${nested_child}///a/test_alarm '{ "severity":"critical","text":"temperature alarm" }' + Cumulocity.Device Should Exist ${nested_child} + ${alarm}= Device Should Have Alarm/s type=test_alarm expected_text=temperature alarm severity=CRITICAL minimum=1 maximum=1 + Log ${alarm} + +Nested child devices support sending event + ${nested_child}= Get Random Name + Execute Command tedge mqtt pub --retain 'te/device/${nested_child}//' '{"@type":"child-device","@parent":"device/${CHILD_SN}//","@id":"${nested_child}"}' + Execute Command tedge mqtt pub te/device/${nested_child}///e/event_nested '{ "text":"nested child event" }' + Cumulocity.Device Should Exist ${nested_child} + Device Should Have Event/s expected_text=nested child event type=event_nested minimum=1 maximum=1 + +# Nested child device services +Nested child device service support sending simple measurements + ${nested_child}= Get Random Name + Execute Command tedge mqtt pub --retain 'te/device/${nested_child}//' '{"@type":"child-device","@parent":"device/${CHILD_SN}//","@id":"${nested_child}"}' + Execute Command tedge mqtt pub --retain 'te/device/${nested_child}/service/nested_ms_service' '{"@type":"service","@parent":"device/${nested_child}//","@id":"nested_ms_service"}' + Execute Command tedge mqtt pub te/device/${nested_child}/service/nested_ms_service/m/m_type '{ "temperature": 30.1 }' + Cumulocity.Device Should Exist ${nested_child} + Cumulocity.Should Have Services name=nested_ms_service min_count=1 max_count=1 + Cumulocity.Device Should Exist nested_ms_service + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 + Should Be Equal ${measurements[0]["type"]} m_type + Should Be Equal As Numbers ${measurements[0]["temperature"]["temperature"]["value"]} 30.1 + Log ${measurements} + +Nested child device service support sending events + ${nested_child}= Get Random Name + Execute Command tedge mqtt pub --retain 'te/device/${nested_child}//' '{"@type":"child-device","@parent":"device/${CHILD_SN}//","@id":"${nested_child}"}' + Execute Command tedge mqtt pub --retain 'te/device/${nested_child}/service/nested_event_service' '{"@type":"service","@parent":"device/${nested_child}//","@id":"nested_event_service"}' + Execute Command tedge mqtt pub te/device/${nested_child}/service/nested_event_service/e/e_type '{ "text": "nested device service started" }' + Cumulocity.Device Should Exist ${nested_child} + Cumulocity.Should Have Services name=nested_event_service min_count=1 max_count=1 + Cumulocity.Device Should Exist nested_event_service + Device Should Have Event/s expected_text=nested device service started type=e_type minimum=1 maximum=1 + +Nested child device service support sending alarm + ${nested_child}= Get Random Name + Execute Command tedge mqtt pub --retain 'te/device/${nested_child}//' '{"@type":"child-device","@parent":"device/${CHILD_SN}//","@id":"${nested_child}"}' + Execute Command tedge mqtt pub --retain 'te/device/${nested_child}/service/nested_alarm_service' '{"@type":"service","@parent":"device/${nested_child}//","@id":"nested_alarm_service"}' + Execute Command tedge mqtt pub te/device/${nested_child}/service/nested_alarm_service/a/test_alarm '{ "severity":"critical","text":"temperature alarm" }' + Cumulocity.Device Should Exist ${nested_child} + Cumulocity.Should Have Services name=nested_alarm_service min_count=1 max_count=1 + Cumulocity.Device Should Exist nested_alarm_service + ${alarm}= Device Should Have Alarm/s type=test_alarm expected_text=temperature alarm severity=CRITICAL minimum=1 maximum=1 + Log ${alarm} + *** Keywords *** Custom Setup diff --git a/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry.robot b/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry.robot index 4360e743d76..740779130cd 100644 --- a/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry.robot +++ b/tests/RobotFramework/tests/cumulocity/telemetry/thin-edge_device_telemetry.robot @@ -152,6 +152,69 @@ Thin-edge device supports sending inventory data via tedge topic to root fragmen Should Be Equal ${mo["subtype"]} LinuxDeviceA Should Be Equal ${mo["type"]} thin-edge.io Should Be Equal ${mo["name"]} ${DEVICE_SN} +# +# Services +# +# measurements +Send measurements to an unregistered service + Execute Command tedge mqtt pub te/device/main/service/app1/m/service_type001 '{"temperature": 30.1}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services min_count=1 max_count=1 name=app1 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app1 + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=service_type001 + Should Be Equal ${measurements[0]["type"]} service_type001 + Should Be Equal As Numbers ${measurements[0]["temperature"]["temperature"]["value"]} 30.1 + +Send measurements to a registered service + Execute Command tedge mqtt pub --retain te/device/main/service/app2 '{"@type":"service","@parent":"device/main//"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services name=app2 min_count=1 max_count=1 + + Execute Command tedge mqtt pub te/device/main/service/app2/m/service_type002 '{"temperature": 30.1}' + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app2 + ${measurements}= Device Should Have Measurements minimum=1 maximum=1 type=service_type002 + Should Be Equal ${measurements[0]["type"]} service_type002 + Should Be Equal As Numbers ${measurements[0]["temperature"]["temperature"]["value"]} 30.1 + +# alarms +Send alarms to an unregistered service + Execute Command tedge mqtt pub te/device/main/service/app3/a/alarm_001 '{"text": "test alarm","severity":"major"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services min_count=1 max_count=1 name=app3 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app3 + ${alarms}= Device Should Have Alarm/s expected_text=test alarm type=alarm_001 minimum=1 maximum=1 + Should Be Equal ${alarms[0]["type"]} alarm_001 + Should Be Equal ${alarms[0]["severity"]} MAJOR + +Send alarms to a registered service + Execute Command tedge mqtt pub --retain te/device/main/service/app4 '{"@type":"service","@parent":"device/main//"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services name=app4 min_count=1 max_count=1 + + Execute Command tedge mqtt pub te/device/main/service/app4/a/alarm_002 '{"text": "test alarm"}' + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app4 + ${alarms}= Device Should Have Alarm/s expected_text=test alarm type=alarm_002 minimum=1 maximum=1 + Should Be Equal ${alarms[0]["type"]} alarm_002 + +# events +Send events to an unregistered service + Execute Command tedge mqtt pub te/device/main/service/app5/e/event_001 '{"text": "test event"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services name=app5 min_count=1 max_count=1 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app5 + Device Should Have Event/s expected_text=test event type=event_001 minimum=1 maximum=1 + +Send events to a registered service + Execute Command tedge mqtt pub --retain te/device/main/service/app6 '{"@type":"service","@parent":"device/main//"}' + Cumulocity.Device Should Exist ${DEVICE_SN} + Cumulocity.Should Have Services name=app6 min_count=1 max_count=1 + + Cumulocity.Device Should Exist ${DEVICE_SN}:device:main:service:app6 + Execute Command tedge mqtt pub te/device/main/service/app6/e/event_002 '{"text": "test event"}' + Device Should Have Event/s expected_text=test event type=event_002 minimum=1 maximum=1 *** Keywords ***