diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index 2d5ed1e47ac..58521b06f50 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -578,7 +578,20 @@ define_tedge_config! { /// Auto-upload the operation log once it finishes. #[tedge_config(example = "always", example = "never", example = "on-failure", default(variable = "AutoLogUpload::Never"))] auto_log_upload: AutoLogUpload, - } + }, + + availability: { + /// Enable sending heartbeat to Cumulocity periodically. If set to false, c8y_RequiredAvailability won't be sent + #[tedge_config(example = "true", default(value = true))] + enable: bool, + + /// Heartbeat interval to be sent to Cumulocity as c8y_RequiredAvailability. + /// The value must be greater than 1 minute. + /// If set to a lower value or 0, the device is considered in maintenance mode in the Cumulocity context. + /// Details: https://cumulocity.com/docs/device-integration/fragment-library/#device-availability + #[tedge_config(example = "60m", default(from_str = "60m"))] + interval: SecondsOrHumanTime, + }, }, #[tedge_config(deprecated_name = "azure")] // for 0.1.0 compatibility diff --git a/crates/core/c8y_api/src/smartrest/inventory.rs b/crates/core/c8y_api/src/smartrest/inventory.rs index 291370773b1..0bd878f6fc0 100644 --- a/crates/core/c8y_api/src/smartrest/inventory.rs +++ b/crates/core/c8y_api/src/smartrest/inventory.rs @@ -11,7 +11,9 @@ use crate::smartrest::csv::fields_to_csv_string; use crate::smartrest::topic::publish_topic_from_ancestors; +use crate::smartrest::topic::C8yTopic; use mqtt_channel::MqttMessage; +use std::time::Duration; use tedge_config::TopicPrefix; /// Create a SmartREST message for creating a child device under the given ancestors. @@ -116,6 +118,29 @@ pub fn service_creation_message_payload( ])) } +/// Create a SmartREST message to set a response interval for c8y_RequiredAvailability. +/// +/// In the SmartREST 117 message, the interval must be in MINUTES, and can be <=0, +/// which means the device is in maintenance mode in the c8y context. +/// Details: https://cumulocity.com/docs/device-integration/fragment-library/#device-availability +#[derive(Debug)] +pub struct C8ySmartRestSetInterval117 { + pub c8y_topic: C8yTopic, + pub interval: Duration, + pub prefix: TopicPrefix, +} + +impl From for MqttMessage { + fn from(value: C8ySmartRestSetInterval117) -> Self { + let topic = value.c8y_topic.to_topic(&value.prefix).unwrap(); + let interval_in_minutes = value.interval.as_secs() / 60; + MqttMessage::new( + &topic, + fields_to_csv_string(&["117", &interval_in_minutes.to_string()]), + ) + } +} + #[derive(thiserror::Error, Debug)] #[error("Field `{field_name}` contains invalid value: {value:?}")] pub struct InvalidValueError { diff --git a/crates/core/tedge_api/src/mqtt_topics.rs b/crates/core/tedge_api/src/mqtt_topics.rs index 3c9fa9a4e33..bedbd533e46 100644 --- a/crates/core/tedge_api/src/mqtt_topics.rs +++ b/crates/core/tedge_api/src/mqtt_topics.rs @@ -168,6 +168,7 @@ impl MqttSchema { }; let channel = match channel { ChannelFilter::EntityMetadata => "".to_string(), + ChannelFilter::EntityTwinData => "/twin/+".to_string(), ChannelFilter::Measurement => "/m/+".to_string(), ChannelFilter::MeasurementMetadata => "/m/+/meta".to_string(), ChannelFilter::Event => "/e/+".to_string(), @@ -178,6 +179,7 @@ impl MqttSchema { ChannelFilter::Command(operation) => format!("/cmd/{operation}/+"), ChannelFilter::AnyCommandMetadata => "/cmd/+".to_string(), ChannelFilter::CommandMetadata(operation) => format!("/cmd/{operation}"), + ChannelFilter::Health => "/status/health".to_string(), }; TopicFilter::new_unchecked(&format!("{}/{entity}{channel}", self.root)) @@ -404,6 +406,11 @@ impl EntityTopicId { self == &Self::default_main_device() } + /// Returns true if the current topic identifier matches that of the service + pub fn is_default_service(&self) -> bool { + self.default_service_name().is_some() + } + /// If `self` is a device topic id, return a service topic id under this /// device. /// @@ -512,7 +519,7 @@ pub enum TopicIdError { /// A channel identifies the type of the messages exchanged over a topic /// /// -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Channel { EntityMetadata, EntityTwinData { @@ -615,6 +622,10 @@ impl Display for Channel { } impl Channel { + pub fn is_entity_metadata(&self) -> bool { + matches!(self, Channel::EntityMetadata) + } + pub fn is_measurement(&self) -> bool { matches!(self, Channel::Measurement { .. }) } @@ -716,8 +727,10 @@ pub enum EntityFilter<'a> { Entity(&'a EntityTopicId), } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ChannelFilter { EntityMetadata, + EntityTwinData, Measurement, Event, Alarm, @@ -728,6 +741,34 @@ pub enum ChannelFilter { AlarmMetadata, AnyCommandMetadata, CommandMetadata(OperationType), + Health, +} + +impl From<&Channel> for ChannelFilter { + fn from(value: &Channel) -> Self { + match value { + Channel::EntityMetadata => ChannelFilter::EntityMetadata, + Channel::EntityTwinData { fragment_key: _ } => ChannelFilter::EntityTwinData, + Channel::Measurement { + measurement_type: _, + } => ChannelFilter::Measurement, + Channel::Event { event_type: _ } => ChannelFilter::Event, + Channel::Alarm { alarm_type: _ } => ChannelFilter::Alarm, + Channel::Command { + operation, + cmd_id: _, + } => ChannelFilter::Command(operation.clone()), + Channel::MeasurementMetadata { + measurement_type: _, + } => ChannelFilter::MeasurementMetadata, + Channel::EventMetadata { event_type: _ } => ChannelFilter::EventMetadata, + Channel::AlarmMetadata { alarm_type: _ } => ChannelFilter::AlarmMetadata, + Channel::CommandMetadata { operation } => { + ChannelFilter::CommandMetadata(operation.clone()) + } + Channel::Health => ChannelFilter::Health, + } + } } pub struct IdGenerator { diff --git a/crates/core/tedge_api/src/store/pending_entity_store.rs b/crates/core/tedge_api/src/store/pending_entity_store.rs index 3393f083fc5..3a47bfecc1d 100644 --- a/crates/core/tedge_api/src/store/pending_entity_store.rs +++ b/crates/core/tedge_api/src/store/pending_entity_store.rs @@ -47,6 +47,15 @@ pub struct PendingEntityData { pub data_messages: Vec, } +impl From for PendingEntityData { + fn from(reg_message: EntityRegistrationMessage) -> Self { + Self { + reg_message, + data_messages: vec![], + } + } +} + impl PendingEntityStore { pub fn new(mqtt_schema: MqttSchema, telemetry_cache_size: usize) -> Self { Self { diff --git a/crates/core/tedge_api/store/message_log.rs b/crates/core/tedge_api/store/message_log.rs deleted file mode 100644 index 7c86a72d929..00000000000 --- a/crates/core/tedge_api/store/message_log.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! The message log is a persistent append-only log of MQTT messages. -//! Each line is the JSON representation of that MQTT message. -//! The underlying file is a JSON lines file. -use mqtt_channel::MqttMessage; -use serde_json::json; -use std::fs::File; -use std::fs::OpenOptions; -use std::io::BufRead; -use std::io::BufReader; -use std::io::BufWriter; -use std::io::Write; -use std::path::Path; - -const LOG_FILE_NAME: &str = "entity_store.jsonl"; -const LOG_FORMAT_VERSION: &str = "1.0"; - -#[derive(thiserror::Error, Debug)] -pub(crate) enum LogEntryError { - #[error(transparent)] - FromStdIo(std::io::Error), - - #[error("Deserialization failed with {0} while parsing {1}")] - FromSerdeJson(#[source] serde_json::Error, String), -} - -/// A reader to read the log file entries line by line -pub(crate) struct MessageLogReader { - reader: BufReader, -} - -impl MessageLogReader { - pub fn new

(log_dir: P) -> Result - where - P: AsRef, - { - let file = OpenOptions::new() - .read(true) - .open(log_dir.as_ref().join(LOG_FILE_NAME))?; - let mut reader = BufReader::new(file); - - let mut version_info = String::new(); - reader.read_line(&mut version_info)?; - // TODO: Validate if the read version is supported - - Ok(MessageLogReader { reader }) - } - - /// Return the next MQTT message from the log - /// The reads start from the beginning of the file - /// and each read advances the file pointer to the next line - pub fn next_message(&mut self) -> Result, LogEntryError> { - let mut buffer = String::new(); - match self.reader.read_line(&mut buffer) { - Ok(bytes_read) if bytes_read > 0 => { - let message: MqttMessage = serde_json::from_str(&buffer) - .map_err(|err| LogEntryError::FromSerdeJson(err, buffer))?; - Ok(Some(message)) - } - Ok(_) => Ok(None), // EOF - Err(err) => Err(LogEntryError::FromStdIo(err)), - } - } -} - -/// A writer to append new MQTT messages to the end of the log -pub struct MessageLogWriter { - writer: BufWriter, -} - -impl MessageLogWriter { - pub fn new

(log_dir: P) -> Result - where - P: AsRef, - { - let file = OpenOptions::new() - .create(true) - .append(true) - .open(log_dir.as_ref().join(LOG_FILE_NAME))?; - - // If the file is empty append the version information as a header - let metadata = file.metadata()?; - let file_is_empty = metadata.len() == 0; - - let mut writer = BufWriter::new(file); - - if file_is_empty { - let version_info = json!({ "version": LOG_FORMAT_VERSION }).to_string(); - writeln!(writer, "{}", version_info)?; - } - - Ok(MessageLogWriter { writer }) - } - - pub fn new_truncated

(log_dir: P) -> Result - where - P: AsRef, - { - let _ = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(log_dir.as_ref().join(LOG_FILE_NAME))?; - - MessageLogWriter::new(log_dir) - } - - /// Append the JSON representation of the given message to the log. - /// Each message is appended on a new line. - pub fn append_message(&mut self, message: &MqttMessage) -> Result<(), std::io::Error> { - let json_line = serde_json::to_string(message)?; - writeln!(self.writer, "{}", json_line)?; - self.writer.flush()?; - self.writer.get_ref().sync_all()?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use crate::message_log::MessageLogReader; - - use super::MessageLogWriter; - use mqtt_channel::MqttMessage; - use mqtt_channel::Topic; - use tempfile::tempdir; - - #[test] - fn test_append_and_retrieve() { - let temp_dir = tempdir().unwrap(); - - // Prepare some dummy messages - let mut messages = vec![]; - for i in 1..5 { - let message = MqttMessage::new( - &Topic::new(&format!("topic{i}")).unwrap(), - format!("payload{i}"), - ); - messages.push(message); - } - - // Populate the log - { - let mut message_log = MessageLogWriter::new(&temp_dir).unwrap(); - let mut message_log_reader = MessageLogReader::new(&temp_dir).unwrap(); - - assert_eq!(message_log_reader.next_message().unwrap(), None); - - for message in messages.clone() { - message_log.append_message(&message).unwrap(); - } - } - - // Read from the log - { - // Reload the message log - let mut message_log_reader = MessageLogReader::new(&temp_dir).unwrap(); - - for message in messages { - assert_eq!(message_log_reader.next_message().unwrap(), Some(message)); - } - // EOF -> None - assert_eq!(message_log_reader.next_message().unwrap(), None); - } - } -} diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index 0ae274e133d..1331c28436d 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -6,6 +6,8 @@ use c8y_auth_proxy::actor::C8yAuthProxyBuilder; use c8y_http_proxy::credentials::C8YJwtRetriever; use c8y_http_proxy::C8YHttpProxyBuilder; use c8y_mapper_ext::actor::C8yMapperBuilder; +use c8y_mapper_ext::availability::AvailabilityBuilder; +use c8y_mapper_ext::availability::AvailabilityConfig; use c8y_mapper_ext::compatibility_adapter::OldAgentAdapter; use c8y_mapper_ext::config::C8yMapperConfig; use c8y_mapper_ext::converter::CumulocityConverter; @@ -209,7 +211,7 @@ impl TEdgeComponent for CumulocityMapper { let mut service_monitor_actor = MqttActorBuilder::new(service_monitor_client_config(&tedge_config)?); - let c8y_mapper_actor = C8yMapperBuilder::try_new( + let mut c8y_mapper_actor = C8yMapperBuilder::try_new( c8y_mapper_config, &mut mqtt_actor, &mut c8y_http_proxy_actor, @@ -224,6 +226,16 @@ impl TEdgeComponent for CumulocityMapper { // and translating the responses received on tedge/commands/res/+/+ to te/device/main///cmd/+/+ let old_to_new_agent_adapter = OldAgentAdapter::builder(&mut mqtt_actor); + let availability_actor = if tedge_config.c8y.availability.enable { + Some(AvailabilityBuilder::new( + AvailabilityConfig::from(&tedge_config), + &mut c8y_mapper_actor, + &mut timer_actor, + )) + } else { + None + }; + runtime.spawn(mqtt_actor).await?; runtime.spawn(jwt_actor).await?; runtime.spawn(http_actor).await?; @@ -236,6 +248,9 @@ impl TEdgeComponent for CumulocityMapper { runtime.spawn(uploader_actor).await?; runtime.spawn(downloader_actor).await?; runtime.spawn(old_to_new_agent_adapter).await?; + if let Some(availability_actor) = availability_actor { + runtime.spawn(availability_actor).await?; + } runtime.run_to_completion().await?; Ok(()) diff --git a/crates/extensions/c8y_mapper_ext/src/actor.rs b/crates/extensions/c8y_mapper_ext/src/actor.rs index 021d7989df1..ef800fb9230 100644 --- a/crates/extensions/c8y_mapper_ext/src/actor.rs +++ b/crates/extensions/c8y_mapper_ext/src/actor.rs @@ -14,6 +14,7 @@ use c8y_auth_proxy::url::ProxyUrlGenerator; use c8y_http_proxy::handle::C8YHttpProxy; use c8y_http_proxy::messages::C8YRestRequest; use c8y_http_proxy::messages::C8YRestResult; +use std::collections::HashMap; use std::path::PathBuf; use std::time::Duration; use tedge_actors::fan_in_message_type; @@ -32,6 +33,10 @@ use tedge_actors::Sender; use tedge_actors::Service; use tedge_actors::SimpleMessageBox; use tedge_actors::SimpleMessageBoxBuilder; +use tedge_api::entity_store::EntityRegistrationMessage; +use tedge_api::mqtt_topics::Channel; +use tedge_api::mqtt_topics::ChannelFilter; +use tedge_api::pending_entity_store::PendingEntityData; use tedge_downloader_ext::DownloadRequest; use tedge_downloader_ext::DownloadResult; use tedge_file_system_ext::FsWatchEvent; @@ -58,7 +63,22 @@ pub(crate) type IdUploadResult = (CmdId, UploadResult); pub(crate) type IdDownloadResult = (CmdId, DownloadResult); pub(crate) type IdDownloadRequest = (CmdId, DownloadRequest); -fan_in_message_type!(C8yMapperInput[MqttMessage, FsWatchEvent, SyncComplete, IdUploadResult, IdDownloadResult] : Debug); +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishMessage(pub MqttMessage); + +impl From for PublishMessage { + fn from(value: MqttMessage) -> Self { + PublishMessage(value) + } +} + +impl From for MqttMessage { + fn from(value: PublishMessage) -> Self { + value.0 + } +} + +fan_in_message_type!(C8yMapperInput[MqttMessage, FsWatchEvent, SyncComplete, IdUploadResult, IdDownloadResult, PublishMessage] : Debug); type C8yMapperOutput = MqttMessage; pub struct C8yMapperActor { @@ -67,6 +87,7 @@ pub struct C8yMapperActor { mqtt_publisher: LoggingSender, timer_sender: LoggingSender, bridge_status_messages: SimpleMessageBox, + message_handlers: HashMap>>, } #[async_trait] @@ -116,6 +137,9 @@ impl Actor for C8yMapperActor { C8yMapperInput::IdDownloadResult((cmd_id, result)) => { self.process_download_result(cmd_id, result).await?; } + C8yMapperInput::PublishMessage(message) => { + self.mqtt_publisher.send(message.0).await?; + } } } Ok(()) @@ -129,6 +153,7 @@ impl C8yMapperActor { mqtt_publisher: LoggingSender, timer_sender: LoggingSender, bridge_status_messages: SimpleMessageBox, + message_handlers: HashMap>>, ) -> Self { Self { converter, @@ -136,19 +161,136 @@ impl C8yMapperActor { mqtt_publisher, timer_sender, bridge_status_messages, + message_handlers, } } + /// Processing an incoming message involves the following steps, if the message follows MQTT topic scheme v1: + /// 1. Try to register the source entity and any of its cached pending children for the incoming message + /// 2. For each entity that got registered in the previous step + /// 1. Convert and publish that registration message + /// 2. Publish that registration messages to any message handlers interested in that message type + /// 3. Convert and publish all the cached data messages of that entity to the cloud + /// 4. Publish those data messages also to any message handlers interested in those message types + /// 3. Once all the required entities and their cached data is processed, process the incoming message itself + /// 1. Convert and publish that message to the cloud + /// 2. Publish that message to any message handlers interested in its message type + /// + /// If the message follows the legacy topic scheme v0, the data message is simply converted the old way. async fn process_mqtt_message(&mut self, message: MqttMessage) -> Result<(), RuntimeError> { - let converted_messages = self.converter.convert(&message).await; + // If incoming message follows MQTT topic scheme v1 + if let Ok((_, channel)) = self.converter.mqtt_schema.entity_channel_of(&message.topic) { + match self.converter.try_register_source_entities(&message).await { + Ok(pending_entities) => { + self.process_registered_entities(pending_entities, &channel) + .await?; + } + Err(err) => { + self.mqtt_publisher + .send(self.converter.new_error_message(err)) + .await?; + return Ok(()); + } + } + + if !channel.is_entity_metadata() { + self.process_message(message.clone()).await?; + } + } else { + self.convert_and_publish(&message).await?; + } + + Ok(()) + } + + /// Process a list of registered entities with their cached data. + /// For each entity its registration message is converted and published to the cloud + /// and any of the interested message handlers for that type, + /// followed by repeating the same for its cached data messages. + async fn process_registered_entities( + &mut self, + pending_entities: Vec, + channel: &Channel, + ) -> Result<(), RuntimeError> { + for pending_entity in pending_entities { + self.process_registration_message(pending_entity.reg_message, channel) + .await?; + + // Convert and publish cached data messages + for pending_data_message in pending_entity.data_messages { + self.process_message(pending_data_message).await?; + } + } + + Ok(()) + } + + async fn process_registration_message( + &mut self, + mut message: EntityRegistrationMessage, + channel: &Channel, + ) -> Result<(), RuntimeError> { + self.converter.append_id_if_not_given(&mut message); + // Convert and publish the registration message + let reg_messages = self + .converter + .convert_entity_registration_message(&message, channel); + self.publish_messages(reg_messages).await?; + + // Send the registration message to all subscribed handlers + self.publish_message_to_subscribed_handles( + &Channel::EntityMetadata, + message + .clone() + .to_mqtt_message(&self.converter.mqtt_schema) + .clone(), + ) + .await?; + + Ok(()) + } - for converted_message in converted_messages.into_iter() { - self.mqtt_publisher.send(converted_message).await?; + // Process an MQTT message by converting and publishing it to the cloud + /// and any of the message handlers interested in its type. + async fn process_message(&mut self, message: MqttMessage) -> Result<(), RuntimeError> { + if let Ok((_, channel)) = self.converter.mqtt_schema.entity_channel_of(&message.topic) { + self.convert_and_publish(&message).await?; + self.publish_message_to_subscribed_handles(&channel, message) + .await?; } Ok(()) } + async fn convert_and_publish(&mut self, message: &MqttMessage) -> Result<(), RuntimeError> { + // Convert and publish the incoming data message + let converted_messages = self.converter.convert(message).await; + self.publish_messages(converted_messages).await?; + + Ok(()) + } + + async fn publish_message_to_subscribed_handles( + &mut self, + channel: &Channel, + message: MqttMessage, + ) -> Result<(), RuntimeError> { + // Send the registration message to all subscribed handlers + if let Some(message_handler) = self.message_handlers.get_mut(&channel.into()) { + for sender in message_handler { + sender.send(message.clone()).await?; + } + } + Ok(()) + } + + async fn publish_messages(&mut self, messages: Vec) -> Result<(), RuntimeError> { + for message in messages.into_iter() { + self.mqtt_publisher.send(message).await?; + } + Ok(()) + } + async fn process_file_watch_event( &mut self, file_event: FsWatchEvent, @@ -314,6 +456,7 @@ pub struct C8yMapperBuilder { download_sender: DynSender, auth_proxy: ProxyUrlGenerator, bridge_monitor_builder: SimpleMessageBoxBuilder, + message_handlers: HashMap>>, } impl C8yMapperBuilder { @@ -357,6 +500,8 @@ impl C8yMapperBuilder { &bridge_monitor_builder, ); + let message_handlers = HashMap::new(); + Ok(Self { config, box_builder, @@ -367,6 +512,7 @@ impl C8yMapperBuilder { download_sender, auth_proxy, bridge_monitor_builder, + message_handlers, }) } @@ -387,6 +533,24 @@ impl RuntimeRequestSink for C8yMapperBuilder { } } +impl MessageSource> for C8yMapperBuilder { + fn connect_sink(&mut self, config: Vec, peer: &impl MessageSink) { + let sender = LoggingSender::new("Mapper MQTT".into(), peer.get_sender()); + for channel in config { + self.message_handlers + .entry(channel) + .or_default() + .push(sender.clone()); + } + } +} + +impl MessageSink for C8yMapperBuilder { + fn get_sender(&self) -> DynSender { + self.box_builder.get_sender().sender_clone() + } +} + impl Builder for C8yMapperBuilder { type Error = RuntimeError; @@ -417,6 +581,7 @@ impl Builder for C8yMapperBuilder { mqtt_publisher, timer_sender, bridge_monitor_box, + self.message_handlers, )) } } diff --git a/crates/extensions/c8y_mapper_ext/src/availability/actor.rs b/crates/extensions/c8y_mapper_ext/src/availability/actor.rs new file mode 100644 index 00000000000..524d958ca56 --- /dev/null +++ b/crates/extensions/c8y_mapper_ext/src/availability/actor.rs @@ -0,0 +1,289 @@ +use crate::availability::AvailabilityConfig; +use crate::availability::AvailabilityInput; +use crate::availability::AvailabilityOutput; +use crate::availability::C8yJsonInventoryUpdate; +use crate::availability::C8ySmartRestSetInterval117; +use crate::availability::TimerStart; +use async_trait::async_trait; +use c8y_api::smartrest::topic::C8yTopic; +use serde_json::json; +use std::collections::HashMap; +use std::str::FromStr; +use tedge_actors::Actor; +use tedge_actors::LoggingSender; +use tedge_actors::MessageReceiver; +use tedge_actors::RuntimeError; +use tedge_actors::Sender; +use tedge_actors::SimpleMessageBox; +use tedge_api::entity_store::EntityExternalId; +use tedge_api::entity_store::EntityRegistrationMessage; +use tedge_api::entity_store::EntityType; +use tedge_api::mqtt_topics::EntityTopicId; +use tedge_api::HealthStatus; +use tedge_api::Status; +use tedge_timer_ext::SetTimeout; +use tracing::debug; +use tracing::info; +use tracing::warn; + +/// The timer payload. Keep it a struct in case if we need more data inside the payload in the future +/// `topic_id` is the EntityTopicId of the target device for availability monitoring +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct TimerPayload { + pub topic_id: EntityTopicId, +} + +/// IDs can be retrieved from the registration message's payload +#[derive(Debug)] +struct DeviceIds { + health_topic_id: EntityTopicId, + external_id: EntityExternalId, +} + +#[derive(Debug)] +enum RegistrationResult { + New, + Update, + Error(String), + Skip(String), +} + +pub struct AvailabilityActor { + config: AvailabilityConfig, + message_box: SimpleMessageBox, + timer_sender: LoggingSender, + device_ids_map: HashMap, + health_status_map: HashMap, +} + +#[async_trait] +impl Actor for AvailabilityActor { + fn name(&self) -> &str { + "AvailabilityActor" + } + + async fn run(mut self) -> Result<(), RuntimeError> { + if !self.config.enable { + info!("Device availability monitoring feature is disabled. To enable it, run 'tedge config set c8y.availability.enable true'"); + return Ok(()); + } + + self.init().await?; + + while let Some(input) = self.message_box.recv().await { + match input { + AvailabilityInput::EntityRegistrationMessage(message) => { + self.process_registration_message(&message).await?; + } + AvailabilityInput::SourceHealthStatus((source, health_status)) => { + self.health_status_map.insert(source, health_status); + } + AvailabilityInput::TimerComplete(event) => { + self.process_timer_complete(event.event).await?; + } + } + } + + Ok(()) + } +} + +impl AvailabilityActor { + pub fn new( + config: AvailabilityConfig, + message_box: SimpleMessageBox, + timer_sender: LoggingSender, + ) -> Self { + Self { + config, + message_box, + timer_sender, + device_ids_map: HashMap::new(), + health_status_map: HashMap::new(), + } + } + + /// Init function to set up for the main device + async fn init(&mut self) -> Result<(), RuntimeError> { + let topic_id = EntityTopicId::default_main_device(); + + self.device_ids_map.insert( + topic_id.clone(), + DeviceIds { + health_topic_id: EntityTopicId::default_main_service("tedge-agent").unwrap(), + external_id: self.config.main_device_id.clone(), + }, + ); + + self.send_smartrest_set_required_availability_for_main_device() + .await?; + self.start_heartbeat_timer(&topic_id).await?; + + Ok(()) + } + + async fn process_registration_message( + &mut self, + message: &EntityRegistrationMessage, + ) -> Result<(), RuntimeError> { + let source = &message.topic_id; + + match message.r#type { + EntityType::MainDevice => match self.update_device_service_pair(message) { + RegistrationResult::New | RegistrationResult::Update => { + self.start_heartbeat_timer(source).await?; + } + RegistrationResult::Error(reason) => warn!(reason), + RegistrationResult::Skip(reason) => debug!(reason), + }, + EntityType::ChildDevice => match self.update_device_service_pair(message) { + RegistrationResult::New => { + self.send_smartrest_set_required_availability_for_child_device(source) + .await?; + self.start_heartbeat_timer(source).await?; + } + RegistrationResult::Update => { + self.start_heartbeat_timer(source).await?; + } + RegistrationResult::Error(reason) => warn!(reason), + RegistrationResult::Skip(reason) => debug!(reason), + }, + EntityType::Service => {} + } + + Ok(()) + } + + /// Insert a <"device topic ID" - "health entity topic ID" and "external ID"> pair into the map. + /// If @health is provided in the registration message, use the value as long as it's valid as entity topic ID. + /// If @health is not provided, use the "tedge-agent" service topic ID as default. + /// @id is the only source to know the device's external ID. Hence, @id must be provided in the registration message. + fn update_device_service_pair( + &mut self, + registration_message: &EntityRegistrationMessage, + ) -> RegistrationResult { + let source = ®istration_message.topic_id; + + let result = match registration_message.other.get("@health") { + None => registration_message + .topic_id + .default_service_for_device("tedge-agent") + .ok_or_else( || format!("The entity is not in the default topic scheme. Please specify '@health' to enable availability monitoring for the device '{source}'")), + Some(raw_value) => match raw_value.as_str() { + None => Err(format!("'@health' must hold a string value. Given: {raw_value:?}, source: {source}")), + Some(maybe_entity_topic_id) => EntityTopicId::from_str(maybe_entity_topic_id) + .map_err(|_| format!("'@health' must be 4-segment identifier like `a/b/c/d`. Given: {maybe_entity_topic_id}, source: {source}")) + } + }; + + match result { + Ok(health_topic_id) => match registration_message.external_id.clone() { + None => RegistrationResult::Skip(format!("Registration message is skipped since '@id' is missing in the payload. source: '{source}', {registration_message:?}")), + Some(external_id) => { + if let Some(ids) = self.device_ids_map.get(source) { + if ids.health_topic_id == health_topic_id { + return RegistrationResult::Skip(format!("Registration message is skipped since no health endpoint change is detected. source: '{source}', {registration_message:?}")) + } + } + + match self.device_ids_map.insert( + source.clone(), + DeviceIds { + health_topic_id, + external_id, + }, + ) { + None => RegistrationResult::New, + Some(_) => RegistrationResult::Update, + } + } + }, + Err(err) => RegistrationResult::Error(err), + } + } + + /// Set a new timer for heartbeat if the given interval is positive value + async fn start_heartbeat_timer(&mut self, source: &EntityTopicId) -> Result<(), RuntimeError> { + if !self.config.interval.is_zero() { + self.timer_sender + .send(SetTimeout::new( + self.config.interval / 2, + TimerPayload { + topic_id: source.clone(), + }, + )) + .await?; + } + + Ok(()) + } + + /// Send SmartREST 117 + /// https://cumulocity.com/docs/smartrest/mqtt-static-templates/#117 + async fn send_smartrest_set_required_availability_for_main_device( + &mut self, + ) -> Result<(), RuntimeError> { + let c8y_117 = C8ySmartRestSetInterval117 { + c8y_topic: C8yTopic::SmartRestResponse, + interval: self.config.interval, + prefix: self.config.c8y_prefix.clone(), + }; + self.message_box.send(c8y_117.into()).await?; + + Ok(()) + } + + /// Send SmartREST 117 + /// https://cumulocity.com/docs/smartrest/mqtt-static-templates/#117 + async fn send_smartrest_set_required_availability_for_child_device( + &mut self, + source: &EntityTopicId, + ) -> Result<(), RuntimeError> { + if let Some(external_id) = self + .device_ids_map + .get(source) + .map(|ids| ids.external_id.clone()) + { + let c8y_117 = C8ySmartRestSetInterval117 { + c8y_topic: C8yTopic::ChildSmartRestResponse(external_id.into()), + interval: self.config.interval, + prefix: self.config.c8y_prefix.clone(), + }; + + self.message_box.send(c8y_117.into()).await?; + } + + Ok(()) + } + + async fn process_timer_complete( + &mut self, + timer_payload: TimerPayload, + ) -> Result<(), RuntimeError> { + let entity_topic_id = timer_payload.topic_id; + if let Some((service_topic_id, external_id)) = self + .device_ids_map + .get(&entity_topic_id) + .map(|ids| (&ids.health_topic_id, ids.external_id.as_ref())) + { + if let Some(health_status) = self.health_status_map.get(service_topic_id) { + // Send an empty JSON over MQTT message if the target service status is "up" + if health_status.status == Status::Up { + let json_over_mqtt = C8yJsonInventoryUpdate { + external_id: external_id.into(), + payload: json!({}), + prefix: self.config.c8y_prefix.clone(), + }; + self.message_box.send(json_over_mqtt.into()).await?; + } else { + debug!("Heartbeat message is not sent because the status of the service '{service_topic_id}' is not 'up'"); + } + } + + // Set a new timer + self.start_heartbeat_timer(&entity_topic_id).await?; + }; + + Ok(()) + } +} diff --git a/crates/extensions/c8y_mapper_ext/src/availability/builder.rs b/crates/extensions/c8y_mapper_ext/src/availability/builder.rs new file mode 100644 index 00000000000..a23fc4a9fde --- /dev/null +++ b/crates/extensions/c8y_mapper_ext/src/availability/builder.rs @@ -0,0 +1,117 @@ +use crate::actor::PublishMessage; +use crate::availability::actor::AvailabilityActor; +use crate::availability::AvailabilityConfig; +use crate::availability::AvailabilityInput; +use crate::availability::AvailabilityOutput; +use crate::availability::TimerComplete; +use crate::availability::TimerStart; +use std::convert::Infallible; +use tedge_actors::Builder; +use tedge_actors::CloneSender; +use tedge_actors::DynSender; +use tedge_actors::LoggingSender; +use tedge_actors::MessageSink; +use tedge_actors::MessageSource; +use tedge_actors::NoConfig; +use tedge_actors::RuntimeRequest; +use tedge_actors::RuntimeRequestSink; +use tedge_actors::Service; +use tedge_actors::SimpleMessageBoxBuilder; +use tedge_api::entity_store::EntityRegistrationMessage; +use tedge_api::mqtt_topics::Channel; +use tedge_api::mqtt_topics::ChannelFilter; +use tedge_api::HealthStatus; +use tedge_mqtt_ext::MqttMessage; + +pub struct AvailabilityBuilder { + config: AvailabilityConfig, + box_builder: SimpleMessageBoxBuilder, + timer_sender: DynSender, +} + +impl AvailabilityBuilder { + pub fn new( + config: AvailabilityConfig, + mqtt: &mut (impl MessageSource> + MessageSink), + timer: &mut impl Service, + ) -> Self { + let mut box_builder: SimpleMessageBoxBuilder = + SimpleMessageBoxBuilder::new("AvailabilityMonitoring", 16); + + box_builder.connect_mapped_source( + Self::channels(), + mqtt, + Self::mqtt_message_parser(config.clone()), + ); + + mqtt.connect_mapped_source(NoConfig, &mut box_builder, Self::mqtt_message_builder()); + + let timer_sender = timer.connect_client(box_builder.get_sender().sender_clone()); + + Self { + config: config.clone(), + box_builder, + timer_sender, + } + } + + pub(crate) fn channels() -> Vec { + vec![ChannelFilter::EntityMetadata, ChannelFilter::Health] + } + + fn mqtt_message_parser( + config: AvailabilityConfig, + ) -> impl Fn(MqttMessage) -> Option { + move |message| { + if let Ok((source, channel)) = config.mqtt_schema.entity_channel_of(&message.topic) { + match channel { + Channel::EntityMetadata => { + if let Ok(registration_message) = + EntityRegistrationMessage::try_from(&message) + { + return Some(registration_message.into()); + } + } + Channel::Health => { + let health_status: HealthStatus = + serde_json::from_slice(message.payload()).unwrap_or_default(); + return Some((source, health_status).into()); + } + _ => {} + } + } + None + } + } + + fn mqtt_message_builder() -> impl Fn(AvailabilityOutput) -> Option { + move |res| match res { + AvailabilityOutput::C8ySmartRestSetInterval117(value) => { + Some(PublishMessage(value.into())) + } + AvailabilityOutput::C8yJsonInventoryUpdate(value) => Some(PublishMessage(value.into())), + } + } +} + +impl RuntimeRequestSink for AvailabilityBuilder { + fn get_signal_sender(&self) -> DynSender { + self.box_builder.get_signal_sender() + } +} + +impl Builder for AvailabilityBuilder { + type Error = Infallible; + + fn try_build(self) -> Result { + Ok(self.build()) + } + + fn build(self) -> AvailabilityActor { + let timer_sender = + LoggingSender::new("AvailabilityActor => Timer".into(), self.timer_sender); + let message_box = self.box_builder.build(); + + AvailabilityActor::new(self.config, message_box, timer_sender) + } +} diff --git a/crates/extensions/c8y_mapper_ext/src/availability/mod.rs b/crates/extensions/c8y_mapper_ext/src/availability/mod.rs new file mode 100644 index 00000000000..6e742b72b7b --- /dev/null +++ b/crates/extensions/c8y_mapper_ext/src/availability/mod.rs @@ -0,0 +1,73 @@ +use crate::availability::actor::TimerPayload; +pub use builder::AvailabilityBuilder; +use c8y_api::smartrest::inventory::C8ySmartRestSetInterval117; +use std::time::Duration; +use tedge_actors::fan_in_message_type; +use tedge_api::entity_store::EntityExternalId; +use tedge_api::entity_store::EntityRegistrationMessage; +use tedge_api::mqtt_topics::EntityTopicId; +use tedge_api::mqtt_topics::MqttSchema; +use tedge_api::HealthStatus; +use tedge_config::TEdgeConfig; +use tedge_config::TopicPrefix; +use tedge_mqtt_ext::MqttMessage; +use tedge_mqtt_ext::Topic; +use tedge_timer_ext::SetTimeout; +use tedge_timer_ext::Timeout; + +mod actor; +mod builder; +#[cfg(test)] +mod tests; + +pub type TimerStart = SetTimeout; +pub type TimerComplete = Timeout; +pub type SourceHealthStatus = (EntityTopicId, HealthStatus); + +fan_in_message_type!(AvailabilityInput[EntityRegistrationMessage, SourceHealthStatus, TimerComplete] : Debug); +fan_in_message_type!(AvailabilityOutput[C8ySmartRestSetInterval117, C8yJsonInventoryUpdate] : Debug); + +// TODO! Make it generic and move to c8y_api crate while refactoring c8y-mapper +#[derive(Debug)] +pub struct C8yJsonInventoryUpdate { + external_id: String, + payload: serde_json::Value, + pub prefix: TopicPrefix, +} + +impl From for MqttMessage { + fn from(value: C8yJsonInventoryUpdate) -> Self { + let json_over_mqtt_topic = format!( + "{prefix}/inventory/managedObjects/update/{external_id}", + prefix = value.prefix, + external_id = value.external_id + ); + MqttMessage::new( + &Topic::new_unchecked(&json_over_mqtt_topic), + value.payload.to_string(), + ) + } +} + +/// Required key-value pairs derived from tedge config +#[derive(Debug, Clone)] +pub struct AvailabilityConfig { + pub main_device_id: EntityExternalId, + pub mqtt_schema: MqttSchema, + pub c8y_prefix: TopicPrefix, + pub enable: bool, + pub interval: Duration, +} + +impl From<&TEdgeConfig> for AvailabilityConfig { + fn from(tedge_config: &TEdgeConfig) -> Self { + let xid = tedge_config.device.id.try_read(tedge_config).unwrap(); + Self { + main_device_id: xid.into(), + mqtt_schema: MqttSchema::with_root(tedge_config.mqtt.topic_root.clone()), + c8y_prefix: tedge_config.c8y.bridge.topic_prefix.clone(), + enable: tedge_config.c8y.availability.enable, + interval: tedge_config.c8y.availability.interval.duration(), + } + } +} diff --git a/crates/extensions/c8y_mapper_ext/src/availability/tests.rs b/crates/extensions/c8y_mapper_ext/src/availability/tests.rs new file mode 100644 index 00000000000..67de6da24f2 --- /dev/null +++ b/crates/extensions/c8y_mapper_ext/src/availability/tests.rs @@ -0,0 +1,447 @@ +use crate::actor::PublishMessage; +use crate::availability::actor::TimerPayload; +use crate::availability::AvailabilityBuilder; +use crate::availability::AvailabilityConfig; +use crate::availability::TimerComplete; +use crate::availability::TimerStart; +use serde_json::json; +use std::time::Duration; +use tedge_actors::test_helpers::FakeServerBox; +use tedge_actors::test_helpers::FakeServerBoxBuilder; +use tedge_actors::test_helpers::MessageReceiverExt; +use tedge_actors::test_helpers::WithTimeout; +use tedge_actors::Actor; +use tedge_actors::Builder; +use tedge_actors::MessageReceiver; +use tedge_actors::Sender; +use tedge_actors::SimpleMessageBox; +use tedge_actors::SimpleMessageBoxBuilder; +use tedge_api::mqtt_topics::EntityTopicId; +use tedge_api::mqtt_topics::MqttSchema; +use tedge_mqtt_ext::test_helpers::assert_received_contains_str; +use tedge_mqtt_ext::test_helpers::assert_received_includes_json; +use tedge_mqtt_ext::MqttMessage; +use tedge_mqtt_ext::Topic; +use tedge_timer_ext::Timeout; + +const TEST_TIMEOUT_MS: Duration = Duration::from_millis(7000); + +#[tokio::test] +async fn main_device_init() { + let config = get_availability_config(10); + let handlers = spawn_availability_actor(config).await; + let mut mqtt = handlers.mqtt_box.with_timeout(TEST_TIMEOUT_MS); + let mut timer = handlers.timer_box; + + // SmartREST + assert_received_contains_str(&mut mqtt, [("c8y/s/us", "117,10")]).await; + + // Timer request + let timer_start = timer_recv(&mut timer).await; + assert_eq!(timer_start.duration, Duration::from_secs(10 * 60 / 2)); + assert_eq!( + timer_start.event, + TimerPayload { + topic_id: EntityTopicId::default_main_device() + } + ); +} + +#[tokio::test] +async fn main_device_sends_heartbeat() { + let config = get_availability_config(10); + let handlers = spawn_availability_actor(config).await; + let mut mqtt = handlers.mqtt_box.with_timeout(TEST_TIMEOUT_MS); + let mut timer = handlers.timer_box; + + mqtt.skip(1).await; // SmartREST 117 + timer_recv(&mut timer).await; // First timer request for main device + + // tedge-agent service is up + let health_message = MqttMessage::new( + &Topic::new_unchecked("te/device/main/service/tedge-agent/status/health"), + json!({"status": "up", "pid": 1234}).to_string(), + ); + mqtt.send(health_message).await.unwrap(); + + // timer fired + timer_send( + &mut timer, + TimerPayload { + topic_id: EntityTopicId::default_main_device(), + }, + ) + .await; + + // JSON over MQTT message + assert_received_includes_json( + &mut mqtt, + [("c8y/inventory/managedObjects/update/test-device", json!({}))], + ) + .await; + + // New timer request + let timer_start = timer_recv(&mut timer).await; + assert_eq!(timer_start.duration, Duration::from_secs(10 * 60 / 2)); + assert_eq!( + timer_start.event, + TimerPayload { + topic_id: EntityTopicId::default_main_device() + } + ); +} + +#[tokio::test] +async fn main_device_does_not_send_heartbeat_when_service_status_is_not_up() { + let config = get_availability_config(10); + let handlers = spawn_availability_actor(config).await; + let mut mqtt = handlers.mqtt_box.with_timeout(TEST_TIMEOUT_MS); + let mut timer = handlers.timer_box; + + mqtt.skip(1).await; // SmartREST 117 + timer_recv(&mut timer).await; // First timer request for main device + + // tedge-agent service is DOWN + let health_message = MqttMessage::new( + &Topic::new_unchecked("te/device/main/service/tedge-agent/status/health"), + json!({"status": "down"}).to_string(), + ); + mqtt.send(health_message).await.unwrap(); + + // timer fired + timer_send( + &mut timer, + TimerPayload { + topic_id: EntityTopicId::default_main_device(), + }, + ) + .await; + + // No MQTT message is sent + assert!(mqtt.recv().await.is_none()); + + // New timer request + let timer_start = timer_recv(&mut timer).await; + assert_eq!(timer_start.duration, Duration::from_secs(10 * 60 / 2)); + assert_eq!( + timer_start.event, + TimerPayload { + topic_id: EntityTopicId::default_main_device() + } + ); +} + +#[tokio::test] +async fn main_device_sends_heartbeat_based_on_custom_endpoint() { + let config = get_availability_config(10); + let handlers = spawn_availability_actor(config).await; + let mut mqtt = handlers.mqtt_box.with_timeout(TEST_TIMEOUT_MS); + let mut timer = handlers.timer_box; + + mqtt.skip(1).await; // SmartREST 117 + timer_recv(&mut timer).await; // First timer request for main device + + // registration message + let registration_message = MqttMessage::new( + &Topic::new_unchecked("te/device/main//"), + json!({"@id": "test-device", "@type": "device", "@health": "device/main/service/foo"}) + .to_string(), + ); + mqtt.send(registration_message).await.unwrap(); + + // custom "foo" service is up + let health_message = MqttMessage::new( + &Topic::new_unchecked("te/device/main/service/foo/status/health"), + json!({"status": "up"}).to_string(), + ); + mqtt.send(health_message).await.unwrap(); + + // timer fired + timer_send( + &mut timer, + TimerPayload { + topic_id: EntityTopicId::default_main_device(), + }, + ) + .await; + + // JSON over MQTT message + assert_received_includes_json( + &mut mqtt, + [("c8y/inventory/managedObjects/update/test-device", json!({}))], + ) + .await; +} + +#[tokio::test] +async fn only_one_timer_created_when_registration_message_indicates_same_health_endpoint() { + let config = get_availability_config(10); + let handlers = spawn_availability_actor(config).await; + let mut mqtt = handlers.mqtt_box.with_timeout(TEST_TIMEOUT_MS); + let mut timer = handlers.timer_box; + + mqtt.skip(1).await; // SmartREST 117 + + // Send the registration messages with the same health endpoint + let registration_message = MqttMessage::new( + &Topic::new_unchecked("te/device/main//"), + json!({"@id": "test-device", "@type": "device"}).to_string(), + ); + mqtt.send(registration_message).await.unwrap(); + + let registration_message = MqttMessage::new( + &Topic::new_unchecked("te/device/main//"), + json!({"@id": "test-device", "@type": "device", "@health": "device/main/service/tedge-agent"}).to_string(), + ); + mqtt.send(registration_message).await.unwrap(); + + let registration_message = MqttMessage::new( + &Topic::new_unchecked("te/device/main//"), + json!({"@id": "test-device", "@type": "device", "foo": "bar"}).to_string(), + ); + mqtt.send(registration_message).await.unwrap(); + + // Only one timer created + assert!(timer.recv().with_timeout(TEST_TIMEOUT_MS).await.is_ok()); + assert!(timer.recv().with_timeout(TEST_TIMEOUT_MS).await.is_err()); +} + +#[tokio::test] +async fn child_device_sends_heartbeat() { + let config = get_availability_config(10); + let handlers = spawn_availability_actor(config).await; + let mut mqtt = handlers.mqtt_box.with_timeout(TEST_TIMEOUT_MS); + let mut timer = handlers.timer_box; + + mqtt.skip(1).await; // SmartREST 117 for the main device + timer_recv(&mut timer).await; // First timer request for the main device + + // registration message + let registration_message = MqttMessage::new( + &Topic::new_unchecked("te/device/child1//"), + json!({"@id": "test-device:device:child1", "@type": "child-device"}).to_string(), + ); + mqtt.send(registration_message).await.unwrap(); + + // SmartREST 117 for the child device + assert_received_contains_str( + &mut mqtt, + [("c8y/s/us/test-device:device:child1", "117,10")], + ) + .await; + + // tedge-agent of the child device is up + let health_message = MqttMessage::new( + &Topic::new_unchecked("te/device/child1/service/tedge-agent/status/health"), + json!({"status": "up"}).to_string(), + ); + mqtt.send(health_message).await.unwrap(); + + // timer fired + timer_send( + &mut timer, + TimerPayload { + topic_id: EntityTopicId::default_child_device("child1").unwrap(), + }, + ) + .await; + + // JSON over MQTT message + assert_received_includes_json( + &mut mqtt, + [( + "c8y/inventory/managedObjects/update/test-device:device:child1", + json!({}), + )], + ) + .await; + + // New timer request + let timer_start = timer_recv(&mut timer).await; + assert_eq!(timer_start.duration, Duration::from_secs(10 * 60 / 2)); + assert_eq!( + timer_start.event, + TimerPayload { + topic_id: EntityTopicId::default_child_device("child1").unwrap() + } + ); +} + +#[tokio::test] +async fn child_device_does_not_send_heartbeat_when_service_status_is_not_up() { + let config = get_availability_config(10); + let handlers = spawn_availability_actor(config).await; + let mut mqtt = handlers.mqtt_box.with_timeout(TEST_TIMEOUT_MS); + let mut timer = handlers.timer_box; + + mqtt.skip(1).await; // SmartREST 117 for the main device + timer_recv(&mut timer).await; // First timer request for the main device + + // registration message + let registration_message = MqttMessage::new( + &Topic::new_unchecked("te/device/child1//"), + json!({"@id": "test-device:device:child1", "@type": "child-device"}).to_string(), + ); + mqtt.send(registration_message).await.unwrap(); + + mqtt.skip(1).await; // SmartREST 117 for the child device + + // tedge-agent of the child device is UNKNOWN + let health_message = MqttMessage::new( + &Topic::new_unchecked("te/device/child1/service/tedge-agent/status/health"), + json!({"status": "unknown"}).to_string(), + ); + mqtt.send(health_message).await.unwrap(); + + // timer fired + timer_send( + &mut timer, + TimerPayload { + topic_id: EntityTopicId::default_child_device("child1").unwrap(), + }, + ) + .await; + + // No MQTT message is sent + assert!(mqtt.recv().await.is_none()); + + // New timer request + let timer_start = timer_recv(&mut timer).await; + assert_eq!(timer_start.duration, Duration::from_secs(10 * 60 / 2)); + assert_eq!( + timer_start.event, + TimerPayload { + topic_id: EntityTopicId::default_child_device("child1").unwrap() + } + ); +} + +#[tokio::test] +async fn child_device_sends_heartbeat_based_on_custom_endpoint() { + let config = get_availability_config(10); + let handlers = spawn_availability_actor(config).await; + let mut mqtt = handlers.mqtt_box.with_timeout(TEST_TIMEOUT_MS); + let mut timer = handlers.timer_box; + + mqtt.skip(1).await; // SmartREST 117 for the main device + timer_recv(&mut timer).await; // First timer request for the main device + + // registration message + let registration_message = MqttMessage::new( + &Topic::new_unchecked("te/device/child1//"), + json!({"@id": "test-device:device:child1", "@type": "child-device", "@health": "device/child1/service/foo"}).to_string(), + ); + mqtt.send(registration_message).await.unwrap(); + + // SmartREST 117 for the child device + assert_received_contains_str( + &mut mqtt, + [("c8y/s/us/test-device:device:child1", "117,10")], + ) + .await; + + // Custom service "foo" is up + let health_message = MqttMessage::new( + &Topic::new_unchecked("te/device/child1/service/foo/status/health"), + json!({"status": "up"}).to_string(), + ); + mqtt.send(health_message).await.unwrap(); + + // timer fired + timer_send( + &mut timer, + TimerPayload { + topic_id: EntityTopicId::default_child_device("child1").unwrap(), + }, + ) + .await; + + // JSON over MQTT message + assert_received_includes_json( + &mut mqtt, + [( + "c8y/inventory/managedObjects/update/test-device:device:child1", + json!({}), + )], + ) + .await; +} + +#[tokio::test] +async fn interval_is_zero_value() { + let config = get_availability_config(0); + let handlers = spawn_availability_actor(config).await; + let mut mqtt = handlers.mqtt_box.with_timeout(TEST_TIMEOUT_MS); + let mut timer = handlers.timer_box; + + // SmartREST 117 for the main device + assert_received_contains_str(&mut mqtt, [("c8y/s/us", "117,0")]).await; + + // No timer request created + assert!(timer.recv().with_timeout(TEST_TIMEOUT_MS).await.is_err()); + + // Child registration message + let registration_message = MqttMessage::new( + &Topic::new_unchecked("te/device/child1//"), + json!({"@id": "test-device:device:child1", "@type": "child-device"}).to_string(), + ); + mqtt.send(registration_message).await.unwrap(); + + // SmartREST 117 for the child device + assert_received_contains_str(&mut mqtt, [("c8y/s/us/test-device:device:child1", "117,0")]) + .await; + + // No timer request created + assert!(timer.recv().with_timeout(TEST_TIMEOUT_MS).await.is_err()); +} + +async fn timer_recv(timer: &mut FakeServerBox) -> TimerStart { + timer + .recv() + .with_timeout(TEST_TIMEOUT_MS) + .await + .unwrap() + .unwrap() +} + +async fn timer_send(timer: &mut FakeServerBox, event: TimerPayload) { + timer + .send(Timeout::new(event)) + .with_timeout(TEST_TIMEOUT_MS) + .await + .unwrap() + .unwrap() +} + +struct TestHandler { + pub mqtt_box: SimpleMessageBox, + pub timer_box: FakeServerBox, +} + +async fn spawn_availability_actor(config: AvailabilityConfig) -> TestHandler { + let mut mqtt_builder: SimpleMessageBoxBuilder = + SimpleMessageBoxBuilder::new("MQTT", 10); + let mut timer_builder: FakeServerBoxBuilder = + FakeServerBoxBuilder::default(); + + let availability_builder = + AvailabilityBuilder::new(config, &mut mqtt_builder, &mut timer_builder); + + let actor = availability_builder.build(); + tokio::spawn(async move { actor.run().await }); + + TestHandler { + mqtt_box: mqtt_builder.build(), + timer_box: timer_builder.build(), + } +} + +fn get_availability_config(interval_in_minutes: u64) -> AvailabilityConfig { + AvailabilityConfig { + main_device_id: "test-device".into(), + mqtt_schema: MqttSchema::default(), + c8y_prefix: "c8y".try_into().unwrap(), + enable: true, + interval: Duration::from_secs(interval_in_minutes * 60), + } +} diff --git a/crates/extensions/c8y_mapper_ext/src/converter.rs b/crates/extensions/c8y_mapper_ext/src/converter.rs index 8f82cebb7d2..678e3411ef5 100644 --- a/crates/extensions/c8y_mapper_ext/src/converter.rs +++ b/crates/extensions/c8y_mapper_ext/src/converter.rs @@ -52,7 +52,6 @@ use camino::Utf8Path; use plugin_sm::operation_logs::OperationLogs; use plugin_sm::operation_logs::OperationLogsError; use serde_json::json; -use serde_json::Map; use serde_json::Value; use service_monitor::convert_health_status_message; use std::collections::HashMap; @@ -325,6 +324,81 @@ impl CumulocityConverter { }) } + /// Try to register the target entity and any of its pending children for the incoming message, + /// if that target entity is not already registered with the entity store. + /// + /// For an entity metadata message (aka registration message), + /// an attempt is made to register that entity and any previously cached children of that entity. + /// If the entity can not be registered due to missing parents, it is cached with the entity store to be registered later. + /// + /// For any other data messages, auto-registration of the target entities are attempted when enabled. + /// + /// In both cases, the successfully registered entities, along with their cached data, is returned. + pub async fn try_register_source_entities( + &mut self, + message: &MqttMessage, + ) -> Result, ConversionError> { + if let Ok((source, channel)) = self.mqtt_schema.entity_channel_of(&message.topic) { + match channel { + Channel::EntityMetadata => { + if let Ok(register_message) = EntityRegistrationMessage::try_from(message) { + return self + .try_register_entity_with_pending_children(register_message) + .await; + } + Err(anyhow!( + "Invalid entity registration message received on topic: {}", + message.topic.name + ) + .into()) + } + _ => { + // if the target entity is unregistered, try to register it first using auto-registration + if self.entity_store.get(&source).is_none() + && self.config.enable_auto_register + && source.matches_default_topic_scheme() + { + let auto_registered_entities = self.try_auto_register_entity(&source)?; + Ok(auto_registered_entities + .into_iter() + .map(|reg_msg| reg_msg.into()) + .collect()) + } else { + // On receipt of an unregistered entity data message with custom topic scheme OR + // one with default topic scheme itself when auto registration disabled, + // since it is received before the entity itself is registered, + // cache it in the unregistered entity store to be processed after the entity is registered + self.entity_store.cache_early_data_message(message.clone()); + + Ok(vec![]) + } + } + } + } else { + Ok(vec![]) + } + } + + /// Convert an entity registration message based on the context: + /// that is the kind of message that triggered this registration(channel) + /// The context is relevant here because of the inconsistency in handling the + /// auto-registered source entities of a health status message. + /// For those health messages, the entity registration message is not mapped and ignored + /// as the status message mapping will create the target entity in the cloud + /// with the proper initial state derived from the status message itself. + pub(crate) fn convert_entity_registration_message( + &mut self, + message: &EntityRegistrationMessage, + channel: &Channel, + ) -> Vec { + let c8y_reg_message = match &channel { + Channel::EntityMetadata => self.try_convert_entity_registration(message), + _ => self.try_convert_auto_registered_entity(message, channel), + }; + self.wrap_errors(c8y_reg_message) + } + + /// Convert an entity registration message into its C8y counterpart pub fn try_convert_entity_registration( &mut self, input: &EntityRegistrationMessage, @@ -984,89 +1058,42 @@ impl CumulocityConverter { ) -> Result, ConversionError> { match &channel { Channel::EntityMetadata => { - if let Ok(register_message) = EntityRegistrationMessage::try_from(message) { - return self - .try_register_entity_with_pending_children(®ister_message) - .await; - } - Err(anyhow!( - "Invalid entity registration message received on topic: {}", - message.topic.name - ) - .into()) + // No conversion done here as entity data messages must be converted using pending_entities_from_incoming_message + Ok(vec![]) } _ => { - let mut converted_messages: Vec = vec![]; - // if the target entity is unregistered, try to register it first using auto-registration - if self.entity_store.get(&source).is_none() { - // On receipt of an unregistered entity data message with custom topic scheme OR - // one with default topic scheme itself when auto registration disabled, - // since it is received before the entity itself is registered, - // cache it in the unregistered entity store to be processed after the entity is registered - if !(self.config.enable_auto_register && source.matches_default_topic_scheme()) - { - self.entity_store.cache_early_data_message(message.clone()); - return Ok(vec![]); - } - - let auto_registered_entities = self.try_auto_register_entity(&source)?; - converted_messages = self - .try_convert_auto_registered_entities(auto_registered_entities, &channel)?; - } - let result = self .try_convert_data_message(source, channel, message) .await; - let mut messages = self.wrap_errors(result); - - converted_messages.append(&mut messages); - Ok(converted_messages) + let messages = self.wrap_errors(result); + Ok(messages) } } } - async fn try_register_entity_with_pending_children( + pub(crate) async fn try_register_entity_with_pending_children( &mut self, - register_message: &EntityRegistrationMessage, - ) -> Result, ConversionError> { - let mut mapped_messages = vec![]; + register_message: EntityRegistrationMessage, + ) -> Result, ConversionError> { match self.entity_store.update(register_message.clone()) { Err(e) => { error!("Entity registration failed: {e}"); + Ok(vec![]) } - Ok((affected_entities, pending_entities)) if !affected_entities.is_empty() => { - for pending_entity in pending_entities { - // Register and convert the entity registration first - let mut c8y_message = - self.try_convert_entity_registration(&pending_entity.reg_message)?; - mapped_messages.append(&mut c8y_message); - - // Republish the metadata message with @id if it's not given - let mut updated_metadata_message = - self.append_id_if_not_given(&pending_entity.reg_message); - mapped_messages.append(&mut updated_metadata_message); - - // Process all the cached data messages for that entity - let mut cached_messages = - self.process_cached_entity_data(pending_entity).await?; - mapped_messages.append(&mut cached_messages); - } - - return Ok(mapped_messages); - } - Ok(_) => { - // Handle the case where @id is missing but there are no other changes to the payload - let mut updated_metadata_message = self.append_id_if_not_given(register_message); - mapped_messages.append(&mut updated_metadata_message); - } + Ok((_, pending_entities)) => Ok(pending_entities), } - Ok(mapped_messages) } - fn try_auto_register_entity( + pub(crate) fn try_auto_register_entity( &mut self, source: &EntityTopicId, ) -> Result, ConversionError> { + if !self.config.enable_auto_register { + return Err(ConversionError::AutoRegistrationDisabled( + source.to_string(), + )); + } + let auto_registered_entities = self.entity_store.auto_register_entity(source)?; for auto_registered_entity in &auto_registered_entities { if auto_registered_entity.r#type == EntityType::ChildDevice { @@ -1084,37 +1111,17 @@ impl CumulocityConverter { Ok(auto_registered_entities) } - fn try_convert_auto_registered_entities( - &mut self, - entities: Vec, - channel: &Channel, - ) -> Result, ConversionError> { - let mut converted_messages: Vec = vec![]; - for entity in entities { - // Append the entity registration message itself and its converted c8y form - converted_messages - .append(&mut self.try_convert_auto_registered_entity(&entity, channel)?); - } - Ok(converted_messages) - } - - fn append_id_if_not_given( + pub(crate) fn append_id_if_not_given( &mut self, - register_message: &EntityRegistrationMessage, - ) -> Vec { + register_message: &mut EntityRegistrationMessage, + ) { let source = ®ister_message.topic_id; if register_message.external_id.is_none() { if let Some(metadata) = self.entity_store.get(source) { - let register_message_with_xid = register_message - .clone() - .with_external_id(metadata.external_id.clone()); - let updated_metadata_message = - register_message_with_xid.to_mqtt_message(&self.mqtt_schema); - return vec![updated_metadata_message]; + register_message.external_id = Some(metadata.external_id.clone()); } } - vec![] } async fn try_convert_data_message( @@ -1123,6 +1130,18 @@ impl CumulocityConverter { channel: Channel, message: &MqttMessage, ) -> Result, ConversionError> { + if self.entity_store.get(&source).is_none() + && !(self.config.enable_auto_register && source.matches_default_topic_scheme()) + { + // Since the entity is still not present in the entity store, + // despite an attempt to register the source entity in try_register_source_entities, + // either auto-registration is disabled or a non-default topic scheme is used. + // In either case, the message would have been cached in the entity store as pending entity data. + // Hence just skip the conversion as it will be converted eventually + // once its source entity is registered. + return Ok(vec![]); + } + match &channel { Channel::EntityTwinData { fragment_key } => { self.try_convert_entity_twin_data(&source, message, fragment_key) @@ -1223,23 +1242,6 @@ impl CumulocityConverter { } } - async fn process_cached_entity_data( - &mut self, - cached_entity: PendingEntityData, - ) -> Result, ConversionError> { - let mut converted_messages = vec![]; - for message in cached_entity.data_messages { - let (source, channel) = self.mqtt_schema.entity_channel_of(&message.topic).unwrap(); - converted_messages.append( - &mut self - .try_convert_data_message(source, channel, &message) - .await?, - ); - } - - Ok(converted_messages) - } - fn validate_operation_supported( &self, op_type: &OperationType, @@ -1258,13 +1260,17 @@ impl CumulocityConverter { /// Return the MQTT representation of the entity registration message itself /// along with its converted c8y equivalent. - fn try_convert_auto_registered_entity( + pub(crate) fn try_convert_auto_registered_entity( &mut self, registration_message: &EntityRegistrationMessage, channel: &Channel, ) -> Result, ConversionError> { let mut registration_messages = vec![]; - registration_messages.push(self.convert_entity_registration_message(registration_message)); + registration_messages.push( + registration_message + .clone() + .to_mqtt_message(&self.mqtt_schema), + ); if registration_message.r#type == EntityType::Service && channel.is_health() { // If the auto-registration is done on a health status message, // no need to map it to a C8y service creation message here, @@ -1280,38 +1286,6 @@ impl CumulocityConverter { Ok(registration_messages) } - fn convert_entity_registration_message( - &self, - value: &EntityRegistrationMessage, - ) -> MqttMessage { - let entity_topic_id = value.topic_id.clone(); - - let mut register_payload: Map = Map::new(); - - let entity_type = match value.r#type { - EntityType::MainDevice => "device", - EntityType::ChildDevice => "child-device", - EntityType::Service => "service", - }; - register_payload.insert("@type".into(), Value::String(entity_type.to_string())); - - if let Some(external_id) = &value.external_id { - register_payload.insert("@id".into(), Value::String(external_id.as_ref().into())); - } - - if let Some(parent_id) = &value.parent { - register_payload.insert("@parent".into(), Value::String(parent_id.to_string())); - } - - register_payload.extend(value.other.clone()); - - MqttMessage::new( - &Topic::new(&format!("{}/{entity_topic_id}", self.mqtt_schema.root)).unwrap(), - serde_json::to_string(&Value::Object(register_payload)).unwrap(), - ) - .with_retain() - } - async fn try_convert_tedge_and_c8y_topics( &mut self, message: &MqttMessage, @@ -1872,7 +1846,6 @@ pub(crate) mod tests { use crate::config::C8yMapperConfig; use crate::Capabilities; use anyhow::Result; - use assert_json_diff::assert_json_eq; use assert_json_diff::assert_json_include; use c8y_api::json_c8y_deserializer::C8yDeviceControlTopic; use c8y_api::smartrest::operations::ResultFormat; @@ -1893,14 +1866,14 @@ pub(crate) mod tests { use tedge_actors::MessageReceiver; use tedge_actors::Sender; use tedge_actors::SimpleMessageBoxBuilder; - use tedge_api::entity_store::EntityRegistrationMessage; - use tedge_api::entity_store::EntityType; use tedge_api::entity_store::InvalidExternalIdError; + use tedge_api::mqtt_topics::Channel; use tedge_api::mqtt_topics::ChannelFilter; use tedge_api::mqtt_topics::EntityFilter; use tedge_api::mqtt_topics::EntityTopicId; use tedge_api::mqtt_topics::MqttSchema; use tedge_api::mqtt_topics::OperationType; + use tedge_api::pending_entity_store::PendingEntityData; use tedge_api::SoftwareUpdateCommand; use tedge_config::AutoLogUpload; use tedge_config::SoftwareManagementApiFlag; @@ -1975,29 +1948,10 @@ pub(crate) mod tests { let alarm_payload = r#"{ "severity": "critical", "text": "Temperature very high" }"#; let alarm_message = MqttMessage::new(&Topic::new_unchecked(alarm_topic), alarm_payload); - // Child device creation messages are published. - let device_creation_msgs = converter.convert(&alarm_message).await; - assert_eq!( - device_creation_msgs[0].topic.name, - "te/device/external_sensor//" - ); - assert_json_eq!( - serde_json::from_str::( - device_creation_msgs[0].payload_str().unwrap() - ) - .unwrap(), - json!({ - "@type":"child-device", - "@id":"test-device:device:external_sensor", - "name": "external_sensor" - }) - ); - - let second_msg = MqttMessage::new( - &Topic::new_unchecked("c8y/s/us"), - "101,test-device:device:external_sensor,external_sensor,thin-edge.io-child", - ); - assert_eq!(device_creation_msgs[1], second_msg); + converter + .try_register_source_entities(&alarm_message) + .await + .unwrap(); // During the sync phase, alarms are not converted immediately, but only cached to be synced later assert!(converter.convert(&alarm_message).await.is_empty()); @@ -2046,8 +2000,101 @@ pub(crate) mod tests { assert!(converter.convert(&internal_alarm_message).await.is_empty()); } + #[test_case( + "m/env", + json!({ "temp": 1}) + ;"measurement" + )] + #[test_case( + "e/click", + json!({ "text": "Someone clicked" }) + ;"event" + )] + #[test_case( + "a/temp", + json!({ "text": "Temperature too high" }) + ;"alarm" + )] + #[test_case( + "twin/custom", + json!({ "foo": "bar" }) + ;"twin" + )] + #[test_case( + "status/health", + json!({ "status": "up" }) + ;"health status" + )] + #[test_case( + "cmd/restart", + json!({ }) + ;"command metadata" + )] + #[test_case( + "cmd/restart/123", + json!({ "status": "init" }) + ;"command" + )] #[tokio::test] - async fn convert_measurement_with_child_id() { + async fn auto_registration(channel: &str, payload: Value) { + let tmp_dir = TempTedgeDir::new(); + let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; + + // Validate auto-registration of child device + let topic = format!("te/device/child1///{channel}"); + let in_message = MqttMessage::new(&Topic::new_unchecked(&topic), payload.to_string()); + + let entities = converter + .try_register_source_entities(&in_message) + .await + .unwrap(); + let messages: Vec = entities + .into_iter() + .map(|entity| entity.reg_message.to_mqtt_message(&converter.mqtt_schema)) + .collect(); + + assert_messages_matching( + &messages, + [( + "te/device/child1//", + json!({ + "@type":"child-device", + "@id":"test-device:device:child1", + "name":"child1" + }) + .into(), + )], + ); + + // Validate auto-registration of child device and its service + let topic = format!("te/device/child2///{channel}"); + let in_message = MqttMessage::new(&Topic::new_unchecked(&topic), payload.to_string()); + + let entities = converter + .try_register_source_entities(&in_message) + .await + .unwrap(); + let messages: Vec = entities + .into_iter() + .map(|entity| entity.reg_message.to_mqtt_message(&converter.mqtt_schema)) + .collect(); + + assert_messages_matching( + &messages, + [( + "te/device/child2//", + json!({ + "@type":"child-device", + "@id":"test-device:device:child2", + "name":"child2" + }) + .into(), + )], + ); + } + + #[tokio::test] + async fn convert_child_device_registration() { let tmp_dir = TempTedgeDir::new(); let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; @@ -2059,8 +2106,18 @@ pub(crate) mod tests { }) .to_string(), ); + let entities = converter + .try_register_source_entities(&in_message) + .await + .unwrap(); - let messages = converter.convert(&in_message).await; + assert_eq!(entities.len(), 1); + let messages = converter.convert_entity_registration_message( + &entities.get(0).unwrap().reg_message, + &Channel::Measurement { + measurement_type: "".into(), + }, + ); assert_messages_matching( &messages, @@ -2078,27 +2135,52 @@ pub(crate) mod tests { "c8y/s/us", "101,test-device:device:child1,child1,thin-edge.io-child".into(), ), - ( - "c8y/measurement/measurements/create", - json!({ - "externalSource":{ - "externalId":"test-device:device:child1", - "type":"c8y_Serial" - }, - "temp":{ - "temp":{ - "value":1.0 - } - }, - "time":"2021-11-16T17:45:40.571760714+01:00", - "type":"ThinEdgeMeasurement" - }) - .into(), - ), ], ); } + #[tokio::test] + async fn convert_measurement_with_child_id() { + let tmp_dir = TempTedgeDir::new(); + let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; + + let in_message = MqttMessage::new( + &Topic::new_unchecked("te/device/child1///m/"), + json!({ + "temp": 1, + "time": "2021-11-16T17:45:40.571760714+01:00" + }) + .to_string(), + ); + converter + .try_register_source_entities(&in_message) + .await + .unwrap(); + + let messages = converter.convert(&in_message).await; + + assert_messages_matching( + &messages, + [( + "c8y/measurement/measurements/create", + json!({ + "externalSource":{ + "externalId":"test-device:device:child1", + "type":"c8y_Serial" + }, + "temp":{ + "temp":{ + "value":1.0 + } + }, + "time":"2021-11-16T17:45:40.571760714+01:00", + "type":"ThinEdgeMeasurement" + }) + .into(), + )], + ); + } + #[tokio::test] async fn convert_measurement_with_nested_child_device() { let tmp_dir = TempTedgeDir::new(); @@ -2112,7 +2194,10 @@ pub(crate) mod tests { }) .to_string(), ); - let _ = converter.convert(®_message).await; + let _ = converter + .try_register_source_entities(®_message) + .await + .unwrap(); let reg_message = MqttMessage::new( &Topic::new_unchecked("te/device/nested_child//"), @@ -2123,7 +2208,10 @@ pub(crate) mod tests { }) .to_string(), ); - let _ = converter.convert(®_message).await; + let _ = converter + .try_register_source_entities(®_message) + .await + .unwrap(); let in_topic = "te/device/nested_child///m/"; let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00"}"#; @@ -2158,7 +2246,10 @@ pub(crate) mod tests { }) .to_string(), ); - let _ = converter.convert(®_message).await; + let _ = converter + .try_register_source_entities(®_message) + .await + .unwrap(); let reg_message = MqttMessage::new( &Topic::new_unchecked("te/device/nested_child//"), @@ -2169,7 +2260,10 @@ pub(crate) mod tests { }) .to_string(), ); - let _ = converter.convert(®_message).await; + let _ = converter + .try_register_source_entities(®_message) + .await + .unwrap(); let reg_message = MqttMessage::new( &Topic::new_unchecked("te/device/nested_child/service/nested_service"), @@ -2180,7 +2274,10 @@ pub(crate) mod tests { }) .to_string(), ); - let _ = converter.convert(®_message).await; + let _ = converter + .try_register_source_entities(®_message) + .await + .unwrap(); 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"}"#; @@ -2211,38 +2308,11 @@ pub(crate) mod tests { let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00"}"#; let in_message = MqttMessage::new(&Topic::new_unchecked(in_topic), in_payload); - let expected_child_create_msg = MqttMessage::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 = MqttMessage::new( - &Topic::new_unchecked("c8y/s/us"), - "101,test-device:device:child1,child1,thin-edge.io-child", - ); - let expected_service_create_msg = MqttMessage::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 _ = converter + .try_register_source_entities(&in_message) + .await + .unwrap(); - let expected_smart_rest_message_service = MqttMessage::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 = MqttMessage::new( &Topic::new_unchecked("c8y/measurement/measurements/create"), json!({ @@ -2256,22 +2326,8 @@ pub(crate) mod tests { .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]); + assert_eq!(out_first_messages, vec![expected_c8y_json_message.clone(),]); } #[tokio::test] @@ -2283,17 +2339,10 @@ pub(crate) mod tests { let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00"}"#; let in_message = MqttMessage::new(&Topic::new_unchecked(in_topic), in_payload); - let expected_create_service_msg = MqttMessage::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 _ = converter + .try_register_source_entities(&in_message) + .await + .unwrap(); let expected_c8y_json_message = MqttMessage::new( &Topic::new_unchecked("c8y/measurement/measurements/create"), @@ -2308,24 +2357,8 @@ pub(crate) mod tests { .to_string(), ); - let expected_smart_rest_message_service = MqttMessage::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]); + assert_eq!(out_first_messages, vec![expected_c8y_json_message.clone(),]); } #[tokio::test] @@ -2378,29 +2411,18 @@ pub(crate) mod tests { let topic = Topic::new_unchecked("te/device/child1///m/"); // First convert invalid Thin Edge JSON message. let invalid_measurement = MqttMessage::new(&topic, "invalid measurement"); + let _ = converter + .try_register_source_entities(&invalid_measurement) + .await + .unwrap(); + let messages = converter.convert(&invalid_measurement).await; assert_messages_matching( &messages, - [ - ( - "te/device/child1//", - json!({ - "@id":"test-device:device:child1", - "@type":"child-device", - "name":"child1", - }) - .into(), - ), - ( - "c8y/s/us", - "101,test-device:device:child1,child1,thin-edge.io-child".into(), - ), - ( - "te/errors", - "Invalid JSON: expected value at line 1 column 1: `invalid measurement\n`" - .into(), - ), - ], + [( + "te/errors", + "Invalid JSON: expected value at line 1 column 1: `invalid measurement\n`".into(), + )], ); // Second convert valid Thin Edge JSON message. @@ -2444,52 +2466,43 @@ pub(crate) mod tests { // First message from "child1" let in_first_message = MqttMessage::new(&Topic::new_unchecked("te/device/child1///m/"), in_payload); + + let _ = converter + .try_register_source_entities(&in_first_message) + .await + .unwrap(); + let out_first_messages: Vec<_> = converter .convert(&in_first_message) .await .into_iter() .filter(|m| m.topic.name.starts_with("c8y")) .collect(); - let expected_first_smart_rest_message = MqttMessage::new( - &Topic::new_unchecked("c8y/s/us"), - "101,test-device:device:child1,child1,thin-edge.io-child", - ); let expected_first_c8y_json_message = MqttMessage::new( &Topic::new_unchecked("c8y/measurement/measurements/create"), r#"{"externalSource":{"externalId":"test-device:device:child1","type":"c8y_Serial"},"temp":{"temp":{"value":1.0}},"time":"2021-11-16T17:45:40.571760714+01:00","type":"ThinEdgeMeasurement"}"#, ); - assert_eq!( - out_first_messages, - vec![ - expected_first_smart_rest_message, - expected_first_c8y_json_message, - ] - ); + assert_eq!(out_first_messages, vec![expected_first_c8y_json_message,]); // Second message from "child2" let in_second_message = MqttMessage::new(&Topic::new_unchecked("te/device/child2///m/"), in_payload); + let _ = converter + .try_register_source_entities(&in_second_message) + .await + .unwrap(); + let out_second_messages: Vec<_> = converter .convert(&in_second_message) .await .into_iter() .filter(|m| m.topic.name.starts_with("c8y")) .collect(); - let expected_second_smart_rest_message = MqttMessage::new( - &Topic::new_unchecked("c8y/s/us"), - "101,test-device:device:child2,child2,thin-edge.io-child", - ); let expected_second_c8y_json_message = MqttMessage::new( &Topic::new_unchecked("c8y/measurement/measurements/create"), r#"{"externalSource":{"externalId":"test-device:device:child2","type":"c8y_Serial"},"temp":{"temp":{"value":1.0}},"time":"2021-11-16T17:45:40.571760714+01:00","type":"ThinEdgeMeasurement"}"#, ); - assert_eq!( - out_second_messages, - vec![ - expected_second_smart_rest_message, - expected_second_c8y_json_message, - ] - ); + assert_eq!(out_second_messages, vec![expected_second_c8y_json_message,]); } #[tokio::test] @@ -2501,6 +2514,11 @@ pub(crate) mod tests { let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00"}"#; let in_message = MqttMessage::new(&Topic::new_unchecked(in_topic), in_payload); + let _ = converter + .try_register_source_entities(&in_message) + .await + .unwrap(); + let expected_c8y_json_message = MqttMessage::new( &Topic::new_unchecked("c8y/measurement/measurements/create"), r#"{"temp":{"temp":{"value":1.0}},"time":"2021-11-16T17:45:40.571760714+01:00","type":"test_type"}"#, @@ -2525,6 +2543,11 @@ pub(crate) mod tests { let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00","type":"type_in_payload"}"#; let in_message = MqttMessage::new(&Topic::new_unchecked(in_topic), in_payload); + let _ = converter + .try_register_source_entities(&in_message) + .await + .unwrap(); + let expected_c8y_json_message = MqttMessage::new( &Topic::new_unchecked("c8y/measurement/measurements/create"), r#"{"temp":{"temp":{"value":1.0}},"time":"2021-11-16T17:45:40.571760714+01:00","type":"type_in_payload"}"#, @@ -2549,10 +2572,10 @@ pub(crate) mod tests { let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00"}"#; let in_message = MqttMessage::new(&Topic::new_unchecked(in_topic), in_payload); - let expected_smart_rest_message = MqttMessage::new( - &Topic::new_unchecked("c8y/s/us"), - "101,test-device:device:child,child,thin-edge.io-child", - ); + let _ = converter + .try_register_source_entities(&in_message) + .await + .unwrap(); let expected_c8y_json_message = MqttMessage::new( &Topic::new_unchecked("c8y/measurement/measurements/create"), @@ -2566,13 +2589,7 @@ pub(crate) mod tests { .into_iter() .filter(|m| m.topic.name.starts_with("c8y")) .collect(); - assert_eq!( - out_messages, - vec![ - expected_smart_rest_message, - expected_c8y_json_message.clone(), - ] - ); + assert_eq!(out_messages, vec![expected_c8y_json_message.clone(),]); } #[tokio::test] @@ -2583,10 +2600,11 @@ pub(crate) mod tests { let in_topic = "te/device/child2///m/test_type"; let in_payload = r#"{"temp": 1, "time": "2021-11-16T17:45:40.571760714+01:00","type":"type_in_payload"}"#; let in_message = MqttMessage::new(&Topic::new_unchecked(in_topic), in_payload); - let expected_smart_rest_message = MqttMessage::new( - &Topic::new_unchecked("c8y/s/us"), - "101,test-device:device:child2,child2,thin-edge.io-child", - ); + + let _ = converter + .try_register_source_entities(&in_message) + .await + .unwrap(); let expected_c8y_json_message = MqttMessage::new( &Topic::new_unchecked("c8y/measurement/measurements/create"), @@ -2600,13 +2618,7 @@ pub(crate) mod tests { .into_iter() .filter(|m| m.topic.name.starts_with("c8y")) .collect(); - assert_eq!( - out_first_messages, - vec![ - expected_smart_rest_message, - expected_c8y_json_message.clone(), - ] - ); + assert_eq!(out_first_messages, vec![expected_c8y_json_message.clone(),]); } #[tokio::test] @@ -2862,135 +2874,21 @@ pub(crate) mod tests { big_measurement_payload, ); + converter + .try_register_source_entities(&big_measurement_message) + .await + .unwrap(); + let result = converter.convert(&big_measurement_message).await; // Skipping the first two auto-registration messages and validating the third mapped message - let payload = result[2].payload_str().unwrap(); + let payload = result[0].payload_str().unwrap(); assert!(payload.starts_with( r#"The payload {"temperature0":0,"temperature1":1,"temperature10" received on te/device/child1///m/ after translation is"# )); assert!(payload.ends_with("greater than the threshold size of 16184.")); } - #[tokio::test] - async fn test_convert_small_measurement_for_child_device() { - let tmp_dir = TempTedgeDir::new(); - let measurement_topic = "te/device/child1///m/"; - let big_measurement_payload = create_thin_edge_measurement(20); // Measurement payload size is 20 bytes - - let big_measurement_message = MqttMessage::new( - &Topic::new_unchecked(measurement_topic), - big_measurement_payload, - ); - let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; - let result: Vec<_> = converter - .convert(&big_measurement_message) - .await - .into_iter() - .filter(|m| m.topic.name.starts_with("c8y")) - .collect(); - - let payload1 = &result[0].payload_str().unwrap(); - let payload2 = &result[1].payload_str().unwrap(); - - assert!(payload1.contains("101,test-device:device:child1,child1,thin-edge.io-child")); - assert!(payload2.contains( - r#"{"externalSource":{"externalId":"test-device:device:child1","type":"c8y_Serial"},"temperature0":{"temperature0":{"value":0.0}},"# - )); - assert!(payload2.contains(r#""type":"ThinEdgeMeasurement""#)); - } - - #[tokio::test] - async fn translate_service_monitor_message_for_child_device() { - let tmp_dir = TempTedgeDir::new(); - let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; - - let in_topic = "te/device/child1/service/child-service-c8y/status/health"; - let in_payload = r#"{"pid":1234,"status":"up","time":"2021-11-16T17:45:40.571760714+01:00","type":"thin-edge.io"}"#; - let in_message = MqttMessage::new(&Topic::new_unchecked(in_topic), in_payload); - - let mqtt_schema = MqttSchema::new(); - let (in_entity, _in_channel) = mqtt_schema.entity_channel_of(&in_message.topic).unwrap(); - - let expected_child_create_smart_rest_message = MqttMessage::new( - &Topic::new_unchecked("c8y/s/us"), - "101,test-device:device:child1,child1,thin-edge.io-child", - ); - - let expected_service_monitor_smart_rest_message = MqttMessage::new( - &Topic::new_unchecked("c8y/s/us/test-device:device:child1"), - r#"102,test-device:device:child1:service:child-service-c8y,service,child-service-c8y,up"#, - ); - - let out_messages = converter.convert(&in_message).await; - let mut out_messages = out_messages.into_iter(); - - // child device entity store registration message - let device_registration_message = out_messages.next().unwrap(); - let device_registration_message = - EntityRegistrationMessage::new(&device_registration_message).unwrap(); - assert_eq!( - device_registration_message.topic_id, - in_entity.default_parent_identifier().unwrap() - ); - assert_eq!(device_registration_message.r#type, EntityType::ChildDevice); - - // child device cloud registration message - assert_eq!( - out_messages.next().unwrap(), - expected_child_create_smart_rest_message - ); - - // service entity store registration message - let service_registration_message = out_messages.next().unwrap(); - let service_registration_message = - EntityRegistrationMessage::new(&service_registration_message).unwrap(); - assert_eq!(service_registration_message.topic_id, in_entity); - assert_eq!(service_registration_message.r#type, EntityType::Service); - - // service cloud registration message - - assert_eq!( - out_messages.next().unwrap(), - expected_service_monitor_smart_rest_message.clone() - ); - } - - #[tokio::test] - async fn translate_service_monitor_message_for_thin_edge_device() { - let tmp_dir = TempTedgeDir::new(); - let (mut converter, _http_proxy) = create_c8y_converter(&tmp_dir).await; - - let in_topic = "te/device/main/service/test-tedge-mapper-c8y/status/health"; - let in_payload = r#"{"pid":1234,"status":"up","time":"2021-11-16T17:45:40.571760714+01:00","type":"thin-edge.io"}"#; - let in_message = MqttMessage::new(&Topic::new_unchecked(in_topic), in_payload); - - let mqtt_schema = MqttSchema::new(); - let (in_entity, _in_channel) = mqtt_schema.entity_channel_of(&in_message.topic).unwrap(); - - let expected_service_monitor_smart_rest_message = MqttMessage::new( - &Topic::new_unchecked("c8y/s/us"), - r#"102,test-device:device:main:service:test-tedge-mapper-c8y,service,test-tedge-mapper-c8y,up"#, - ); - - // Test the output messages contains SmartREST and C8Y JSON. - let mut out_messages = converter.convert(&in_message).await.into_iter(); - - // service entity store registration message - let service_registration_message = out_messages.next().unwrap(); - let service_registration_message = - EntityRegistrationMessage::new(&service_registration_message).unwrap(); - assert_eq!(service_registration_message.topic_id, in_entity); - assert_eq!(service_registration_message.r#type, EntityType::Service); - - let service_monitor_message = out_messages.next().unwrap(); - - assert_eq!( - service_monitor_message, - expected_service_monitor_smart_rest_message - ); - } - #[tokio::test] async fn test_execute_operation_is_not_blocked() { let tmp_dir = TempTedgeDir::new(); @@ -3034,21 +2932,11 @@ pub(crate) mod tests { let mqtt_schema = MqttSchema::default(); let child = EntityTopicId::default_child_device("childId").unwrap(); let child_capability = SoftwareUpdateCommand::capability_message(&mqtt_schema, &child); - let registrations = converter.try_convert(&child_capability).await.unwrap(); - - // the first message should be auto-registration of chidlId - let registration = registrations.get(0).unwrap().clone(); - assert_eq!( - registration, - MqttMessage::new( - &Topic::new_unchecked("te/device/childId//"), - r#"{"@id":"test-device:device:childId","@type":"child-device","name":"childId"}"#, - ) - .with_retain() - ); - // the auto-registration message is produced & processed by the mapper - converter.try_convert(®istration).await.unwrap(); + converter + .try_register_source_entities(&child_capability) + .await + .unwrap(); // A request to a child is forwarded to that child using its registered mapping: external id <=> topic identifier let device_cmd_channel = mqtt_schema.topics( @@ -3161,26 +3049,21 @@ pub(crate) mod tests { r#"{"temperature": 21.37}"#, ); - // when auto-registered, local and cloud registration messages should be produced - let mapped_messages = converter.convert(&measurement_message).await; - - let local_registration_message = mapped_messages - .iter() - .find(|m| EntityRegistrationMessage::new(m).is_some()) + let mut entities = converter + .try_register_source_entities(&measurement_message) + .await .unwrap(); - - // check if cloud registration message - assert!(mapped_messages - .iter() - .any(|m| m.topic.name == "c8y/s/us" && m.payload_str().unwrap().starts_with("102"))); + let local_registration_message = entities.remove(0).reg_message; // when converting a registration message the same as the previous one, no additional registration messages should be produced - let mapped_messages = converter.convert(local_registration_message).await; + let entities = converter + .try_register_source_entities( + &local_registration_message.to_mqtt_message(&MqttSchema::default()), + ) + .await + .unwrap(); - let second_registration_message_mapped = mapped_messages.into_iter().any(|m| { - m.topic.name.starts_with("c8y/s/us") && m.payload_str().unwrap().starts_with("102") - }); - assert!(!second_registration_message_mapped); + assert!(entities.is_empty(), "Duplicate entry not registered"); } #[tokio::test] @@ -3197,6 +3080,11 @@ pub(crate) mod tests { serde_json::to_string(&json!({"status": "up"})).unwrap(), ); + converter + .try_register_source_entities(&service_health_message) + .await + .unwrap(); + let output = converter.convert(&service_health_message).await; let service_creation_message = output .into_iter() @@ -3229,37 +3117,40 @@ pub(crate) mod tests { // Register main device service let _ = converter - .convert(&MqttMessage::new( + .try_register_source_entities(&MqttMessage::new( &Topic::new_unchecked("te/device/main/service/dummy"), json!({ "@type":"service", }) .to_string(), )) - .await; + .await + .unwrap(); // Register immediate child device let _ = converter - .convert(&MqttMessage::new( + .try_register_source_entities(&MqttMessage::new( &Topic::new_unchecked("te/device/immediate_child//"), json!({ "@type":"child-device", }) .to_string(), )) - .await; + .await + .unwrap(); // Register immediate child device service let _ = converter - .convert(&MqttMessage::new( + .try_register_source_entities(&MqttMessage::new( &Topic::new_unchecked("te/device/immediate_child/service/dummy"), json!({ "@type":"service", }) .to_string(), )) - .await; + .await + .unwrap(); // Register nested child device let _ = converter - .convert(&MqttMessage::new( + .try_register_source_entities(&MqttMessage::new( &Topic::new_unchecked("te/device/nested_child//"), json!({ "@type":"child-device", @@ -3267,17 +3158,19 @@ pub(crate) mod tests { }) .to_string(), )) - .await; + .await + .unwrap(); // Register nested child device service let _ = converter - .convert(&MqttMessage::new( + .try_register_source_entities(&MqttMessage::new( &Topic::new_unchecked("te/device/nested_child/service/dummy"), json!({ "@type":"service", }) .to_string(), )) - .await; + .await + .unwrap(); for device_id in ["main", "immediate_child", "nested_child"] { let messages = converter @@ -3313,6 +3206,10 @@ pub(crate) mod tests { &Topic::new_unchecked("te/custom/child1///m/environment"), json!({ "temperature": i }).to_string(), ); + converter + .try_register_source_entities(&measurement_message) + .await + .unwrap(); let mapped_messages = converter.convert(&measurement_message).await; assert!( mapped_messages.is_empty(), @@ -3325,6 +3222,10 @@ pub(crate) mod tests { &Topic::new_unchecked("te/custom/child1///twin/foo"), r#"5.6789"#, ); + converter + .try_register_source_entities(&twin_message) + .await + .unwrap(); let mapped_messages = converter.convert(&twin_message).await; assert!( mapped_messages.is_empty(), @@ -3336,55 +3237,39 @@ pub(crate) mod tests { &Topic::new_unchecked("te/custom/child1//"), json!({"@type": "child-device", "@id": "child1", "name": "child1"}).to_string(), ); - let messages = converter.convert(®_message).await; + + let entities = converter + .try_register_source_entities(®_message) + .await + .unwrap(); + + let messages = pending_entities_into_mqtt_messages(entities); // Assert that the registration message, the twin updates and the cached measurement messages are converted assert_messages_matching( &messages, [ ( - "custom-c8y-prefix/s/us", - "101,child1,child1,thin-edge.io-child".into(), - ), - ( - "custom-c8y-prefix/inventory/managedObjects/update/child1", + "te/custom/child1//", json!({ - "foo": 5.6789 + "@id":"child1", + "@type":"child-device", + "name":"child1" }) .into(), ), + ("te/custom/child1///twin/foo", "5.6789".into()), ( - "custom-c8y-prefix/measurement/measurements/create", - json!({ - "temperature":{ - "temperature":{ - "value": 0.0 - } - }, - }) - .into(), + "te/custom/child1///m/environment", + json!({ "temperature": 0 }).into(), ), ( - "custom-c8y-prefix/measurement/measurements/create", - json!({ - "temperature":{ - "temperature":{ - "value": 1.0 - } - }, - }) - .into(), + "te/custom/child1///m/environment", + json!({ "temperature": 1 }).into(), ), ( - "custom-c8y-prefix/measurement/measurements/create", - json!({ - "temperature":{ - "temperature":{ - "value": 2.0 - } - }, - }) - .into(), + "te/custom/child1///m/environment", + json!({ "temperature": 2 }).into(), ), ], ); @@ -3409,9 +3294,13 @@ pub(crate) mod tests { }) .to_string(), ); - let messages = converter.convert(®_message).await; + + let entities = converter + .try_register_source_entities(®_message) + .await + .unwrap(); assert!( - messages.is_empty(), + entities.is_empty(), "Expected child device registration messages to be cached and not mapped" ); @@ -3426,9 +3315,13 @@ pub(crate) mod tests { }) .to_string(), ); - let messages = converter.convert(®_message).await; + + let entities = converter + .try_register_source_entities(®_message) + .await + .unwrap(); assert!( - messages.is_empty(), + entities.is_empty(), "Expected child device registration messages to be cached and not mapped" ); @@ -3443,97 +3336,57 @@ pub(crate) mod tests { }) .to_string(), ); - let messages = converter.convert(®_message).await; - - // Assert that the registration message, the twin updates and the cached measurement messages are converted + let entities = converter + .try_register_source_entities(®_message) + .await + .unwrap(); + let messages = pending_entities_into_mqtt_messages(entities); assert_messages_matching( &messages, [ - ("c8y/s/us", "101,child0,child0,thin-edge.io-child".into()), ( - "c8y/s/us/child0", - "101,child00,child00,thin-edge.io-child".into(), + "te/device/child0//", + json!({ + "@type": "child-device", + "@id": "child0", + "name": "child0", + "@parent": "device/main//", + }) + .into(), ), ( - "c8y/s/us/child0/child00", - "101,child000,child000,thin-edge.io-child".into(), + "te/device/child00//", + json!({ + "@type": "child-device", + "@id": "child00", + "name": "child00", + "@parent": "device/child0//", + }) + .into(), ), - ], - ); - } - - #[tokio::test] - async fn update_entity_metadata() { - let tmp_dir = TempTedgeDir::new(); - let config = c8y_converter_config(&tmp_dir); - - let (mut converter, _http_proxy) = create_c8y_converter_from_config(config); - - // First register a child device - let reg_message = MqttMessage::new( - &Topic::new_unchecked("te/device/child0//"), - json!({ - "@type": "child-device", - "name": "child0", - "@parent": "device/main//", - }) - .to_string(), - ); - let messages = converter.convert(®_message).await; - - assert_messages_matching( - &messages, - [ - ("c8y/s/us", "101,test-device:device:child0,child0,thin-edge.io-child".into()), ( - "te/device/child0//", - r#"{"@id":"test-device:device:child0","@parent":"device/main//","@type":"child-device","name":"child0"}"#.into(), + "te/device/child000//", + json!({ + "@type": "child-device", + "@id": "child000", + "name": "child000", + "@parent": "device/child00//", + }) + .into(), ), ], ); + } - // Remove "@id" from the payload and republish the registration message - let reg_message = MqttMessage::new( - &Topic::new_unchecked("te/device/child0//"), - json!({ - "@type": "child-device", - "name": "child0", - "@parent": "device/main//", - }) - .to_string(), - ); - let messages = converter.convert(®_message).await; - - assert_messages_matching( - &messages, - [( - "te/device/child0//", - r#"{"@id":"test-device:device:child0","@parent":"device/main//","@type":"child-device","name":"child0"}"#.into(), - )], - ); - - // Add a new field but without "@id" and republish the registration message - let reg_message = MqttMessage::new( - &Topic::new_unchecked("te/device/child0//"), - json!({ - "@type": "child-device", - "name": "child0", - "@parent": "device/main//", - "custom": "foo" - }) - .to_string(), - ); - let messages = converter.convert(®_message).await; - - assert_messages_matching( - &messages, - [ - ("c8y/s/us", "101,test-device:device:child0,child0,thin-edge.io-child".into()), - ( - "te/device/child0//", - r#"{"@id":"test-device:device:child0","@parent":"device/main//","@type":"child-device","custom":"foo","name":"child0"}"#.into(), - )], - ); + fn pending_entities_into_mqtt_messages(entities: Vec) -> Vec { + let mut messages = vec![]; + for entity in entities { + messages.push(entity.reg_message.to_mqtt_message(&MqttSchema::default())); + for data_message in entity.data_messages { + messages.push(data_message); + } + } + messages } pub(crate) async fn create_c8y_converter( diff --git a/crates/extensions/c8y_mapper_ext/src/inventory.rs b/crates/extensions/c8y_mapper_ext/src/inventory.rs index ef54daa9937..6c7f0234102 100644 --- a/crates/extensions/c8y_mapper_ext/src/inventory.rs +++ b/crates/extensions/c8y_mapper_ext/src/inventory.rs @@ -467,29 +467,19 @@ mod tests { r#"{"name":"firmware", "version":"1.0"}"#, ); + converter + .try_register_source_entities(&twin_message) + .await + .unwrap(); + let inventory_messages = converter.convert(&twin_message).await; assert_messages_matching( &inventory_messages, - [ - ( - "te/device/child1//", - json!({ - "@type":"child-device", - "@id":"test-device:device:child1", - "name":"child1" - }) - .into(), - ), - ( - "c8y/s/us", - "101,test-device:device:child1,child1,thin-edge.io-child".into(), - ), - ( - "c8y/inventory/managedObjects/update/test-device:device:child1", - json!({"c8y_Firmware":{"name":"firmware","version":"1.0"}}).into(), - ), - ], + [( + "c8y/inventory/managedObjects/update/test-device:device:child1", + json!({"c8y_Firmware":{"name":"firmware","version":"1.0"}}).into(), + )], ); } } diff --git a/crates/extensions/c8y_mapper_ext/src/lib.rs b/crates/extensions/c8y_mapper_ext/src/lib.rs index ec9efba04c9..bfbd94dc895 100644 --- a/crates/extensions/c8y_mapper_ext/src/lib.rs +++ b/crates/extensions/c8y_mapper_ext/src/lib.rs @@ -1,5 +1,6 @@ pub mod actor; pub mod alarm_converter; +pub mod availability; pub mod compatibility_adapter; pub mod config; pub mod converter; diff --git a/crates/extensions/c8y_mapper_ext/src/operations/config_snapshot.rs b/crates/extensions/c8y_mapper_ext/src/operations/config_snapshot.rs index 14f09d9d9f7..332abe26bff 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/config_snapshot.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/config_snapshot.rs @@ -270,6 +270,7 @@ mod tests { use crate::tests::spawn_c8y_mapper_actor_with_config; use crate::tests::spawn_dummy_c8y_http_proxy; use crate::tests::test_mapper_config; + use crate::tests::TestHandle; use c8y_api::json_c8y_deserializer::C8yDeviceControlTopic; use serde_json::json; use std::time::Duration; @@ -290,7 +291,8 @@ mod tests { #[tokio::test] async fn mapper_converts_config_upload_op_to_config_snapshot_cmd_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -330,7 +332,8 @@ mod tests { #[tokio::test] async fn mapper_converts_config_upload_op_to_config_snapshot_cmd_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -380,7 +383,8 @@ mod tests { #[tokio::test] async fn handle_config_snapshot_executing_and_failed_cmd_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -426,7 +430,8 @@ mod tests { #[tokio::test] async fn handle_config_snapshot_executing_and_failed_cmd_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -486,7 +491,10 @@ mod tests { #[tokio::test] async fn handle_config_snapshot_successful_cmd_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, ul, dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, http, ul, dl, .. + } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -552,7 +560,10 @@ mod tests { #[tokio::test] async fn handle_config_snapshot_successful_cmd_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, ul, dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, http, ul, dl, .. + } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -636,11 +647,11 @@ mod tests { ..test_mapper_config(&ttd) }; let test_handle = spawn_c8y_mapper_actor_with_config(&ttd, config, true).await; - spawn_dummy_c8y_http_proxy(test_handle.c8y_http_box); + spawn_dummy_c8y_http_proxy(test_handle.http); - let mut mqtt = test_handle.mqtt_box.with_timeout(TEST_TIMEOUT_MS); - let mut ul = test_handle.ul_box.with_timeout(TEST_TIMEOUT_MS); - let mut dl = test_handle.dl_box.with_timeout(TEST_TIMEOUT_MS); + let mut mqtt = test_handle.mqtt.with_timeout(TEST_TIMEOUT_MS); + let mut ul = test_handle.ul.with_timeout(TEST_TIMEOUT_MS); + let mut dl = test_handle.dl.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -720,10 +731,10 @@ mod tests { ..test_mapper_config(&ttd) }; let test_handle = spawn_c8y_mapper_actor_with_config(&ttd, config, true).await; - spawn_dummy_c8y_http_proxy(test_handle.c8y_http_box); + spawn_dummy_c8y_http_proxy(test_handle.http); - let mut mqtt = test_handle.mqtt_box.with_timeout(TEST_TIMEOUT_MS); - let mut ul = test_handle.ul_box.with_timeout(TEST_TIMEOUT_MS); + let mut mqtt = test_handle.mqtt.with_timeout(TEST_TIMEOUT_MS); + let mut ul = test_handle.ul.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; diff --git a/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs b/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs index fd89d7b5e73..40e43de0aa1 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/config_update.rs @@ -179,6 +179,7 @@ impl CumulocityConverter { mod tests { use crate::tests::skip_init_messages; use crate::tests::spawn_c8y_mapper_actor; + use crate::tests::TestHandle; use c8y_api::json_c8y_deserializer::C8yDeviceControlTopic; use serde_json::json; use std::time::Duration; @@ -195,7 +196,8 @@ mod tests { #[tokio::test] async fn mapper_converts_config_download_op_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -236,7 +238,8 @@ mod tests { #[tokio::test] async fn mapper_converts_config_download_op_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -287,7 +290,8 @@ mod tests { #[tokio::test] async fn handle_config_update_executing_and_failed_cmd_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -338,7 +342,8 @@ mod tests { #[tokio::test] async fn handle_config_update_executing_and_failed_cmd_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -403,7 +408,8 @@ mod tests { #[tokio::test] async fn handle_config_update_successful_cmd_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -429,7 +435,8 @@ mod tests { #[tokio::test] async fn handle_config_update_successful_cmd_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; diff --git a/crates/extensions/c8y_mapper_ext/src/operations/firmware_update.rs b/crates/extensions/c8y_mapper_ext/src/operations/firmware_update.rs index f3898e6b372..5f198712b7c 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/firmware_update.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/firmware_update.rs @@ -204,7 +204,8 @@ mod tests { #[tokio::test] async fn create_firmware_operation_file_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -226,7 +227,8 @@ mod tests { #[tokio::test] async fn create_firmware_operation_file_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -281,8 +283,10 @@ mod tests { #[tokio::test] async fn mapper_converts_firmware_op_to_firmware_update_cmd_for_main_device() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -325,8 +329,10 @@ mod tests { #[tokio::test] async fn mapper_converts_firmware_op_to_firmware_update_cmd_when_remote_utl_has_c8y_url() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -369,8 +375,10 @@ mod tests { #[tokio::test] async fn mapper_converts_firmware_op_to_firmware_update_cmd_for_child_device() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -423,8 +431,10 @@ mod tests { #[tokio::test] async fn handle_firmware_update_executing_and_failed_cmd_for_main_device() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -471,8 +481,10 @@ mod tests { #[tokio::test] async fn handle_firmware_update_executing_and_failed_cmd_for_child_device() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -529,7 +541,8 @@ mod tests { #[tokio::test] async fn handle_firmware_update_successful_cmd_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -568,7 +581,8 @@ mod tests { #[tokio::test] async fn handle_firmware_update_successful_cmd_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); diff --git a/crates/extensions/c8y_mapper_ext/src/operations/log_upload.rs b/crates/extensions/c8y_mapper_ext/src/operations/log_upload.rs index ef7547595c4..188ae75db30 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/log_upload.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/log_upload.rs @@ -307,8 +307,10 @@ mod tests { #[tokio::test] async fn mapper_converts_smartrest_logfile_req_to_log_upload_cmd_for_main_device() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -354,8 +356,10 @@ mod tests { #[tokio::test] async fn mapper_converts_smartrest_logfile_req_to_log_upload_cmd_for_child_device() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -411,8 +415,9 @@ mod tests { #[tokio::test] async fn mapper_converts_log_upload_cmd_to_supported_op_and_types_for_main_device() { - let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let ttd: TempTedgeDir = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -444,7 +449,8 @@ mod tests { #[tokio::test] async fn mapper_converts_log_upload_cmd_to_supported_op_and_types_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -513,8 +519,10 @@ mod tests { #[tokio::test] async fn handle_log_upload_executing_and_failed_cmd_for_main_device() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -566,8 +574,10 @@ mod tests { #[tokio::test] async fn handle_log_upload_executing_and_failed_cmd_for_child_device() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -650,7 +660,10 @@ mod tests { #[tokio::test] async fn handle_log_upload_successful_cmd_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, ul, dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, http, ul, dl, .. + } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -723,7 +736,10 @@ mod tests { #[tokio::test] async fn handle_log_upload_successful_cmd_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, ul, dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, http, ul, dl, .. + } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); diff --git a/crates/extensions/c8y_mapper_ext/src/operations/mod.rs b/crates/extensions/c8y_mapper_ext/src/operations/mod.rs index 7b6ba35c27f..15ef161ef92 100644 --- a/crates/extensions/c8y_mapper_ext/src/operations/mod.rs +++ b/crates/extensions/c8y_mapper_ext/src/operations/mod.rs @@ -89,6 +89,7 @@ impl CumulocityConverter { mod tests { use crate::tests::skip_init_messages; use crate::tests::spawn_c8y_mapper_actor; + use crate::tests::TestHandle; use std::time::Duration; use tedge_actors::test_helpers::MessageReceiverExt; use tedge_actors::Sender; @@ -102,7 +103,8 @@ mod tests { #[tokio::test] async fn mapper_converts_config_metadata_to_supported_op_and_types_for_main_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -162,7 +164,8 @@ mod tests { #[tokio::test] async fn mapper_converts_config_cmd_to_supported_op_and_types_for_child_device() { let ttd = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&ttd, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; diff --git a/crates/extensions/c8y_mapper_ext/src/tests.rs b/crates/extensions/c8y_mapper_ext/src/tests.rs index ba1179ce102..c5ec3588ef8 100644 --- a/crates/extensions/c8y_mapper_ext/src/tests.rs +++ b/crates/extensions/c8y_mapper_ext/src/tests.rs @@ -6,6 +6,8 @@ use crate::actor::IdDownloadRequest; use crate::actor::IdDownloadResult; use crate::actor::IdUploadRequest; use crate::actor::IdUploadResult; +use crate::actor::PublishMessage; +use crate::availability::AvailabilityBuilder; use crate::Capabilities; use assert_json_diff::assert_json_include; use c8y_api::json_c8y_deserializer::C8yDeviceControlTopic; @@ -26,6 +28,8 @@ use tedge_actors::test_helpers::MessageReceiverExt; use tedge_actors::Actor; use tedge_actors::Builder; use tedge_actors::MessageReceiver; +use tedge_actors::MessageSink; +use tedge_actors::NoConfig; use tedge_actors::NoMessage; use tedge_actors::Sender; use tedge_actors::SimpleMessageBox; @@ -51,8 +55,9 @@ const TEST_TIMEOUT_MS: Duration = Duration::from_millis(3000); #[tokio::test] async fn mapper_publishes_init_messages_on_startup() { // Start SM Mapper - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -95,13 +100,15 @@ async fn mapper_publishes_init_messages_on_startup() { #[tokio::test] async fn child_device_registration_mapping() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = + spawn_c8y_mapper_actor_with_config(&ttd, test_mapper_config(&ttd), true).await; + let mut mqtt = test_handle.mqtt.with_timeout(TEST_TIMEOUT_MS); + let mut timer = test_handle.timer; + let mut avail = test_handle.avail.with_timeout(TEST_TIMEOUT_MS); // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; - - let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; mqtt.send(MqttMessage::new( @@ -116,12 +123,18 @@ async fn child_device_registration_mapping() { [( "c8y/s/us", "101,test-device:device:child1,Child1,RaspberryPi", - ), ( + )], + ) + .await; + + assert_received_contains_str( + &mut avail, + [( "te/device/child1//", r#"{"@id":"test-device:device:child1","@type":"child-device","name":"Child1","type":"RaspberryPi"}"# )], ) - .await; + .await; mqtt.send(MqttMessage::new( &Topic::new_unchecked("te/device/child2//"), @@ -135,12 +148,18 @@ async fn child_device_registration_mapping() { [( "c8y/s/us/test-device:device:child1", "101,test-device:device:child2,test-device:device:child2,thin-edge.io-child", - ), ( + )], + ) + .await; + + assert_received_contains_str( + &mut avail, + [( "te/device/child2//", r#"{"@id":"test-device:device:child2","@parent":"device/child1//","@type":"child-device"}"# )], ) - .await; + .await; mqtt.send(MqttMessage::new( &Topic::new_unchecked("te/device/child3//"), @@ -157,12 +176,24 @@ async fn child_device_registration_mapping() { )], ) .await; + + assert_received_contains_str( + &mut avail, + [( + "te/device/child3//", + r#"{"@id":"child3","@parent":"device/child2//","@type":"child-device"}"#, + )], + ) + .await; } #[tokio::test] async fn custom_topic_scheme_registration_mapping() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -180,10 +211,9 @@ async fn custom_topic_scheme_registration_mapping() { assert_received_contains_str( &mut mqtt, - [("c8y/s/us", "101,test-device:custom,Child1,RaspberryPi"), - ("te/custom///", r#"{"@id":"test-device:custom","@type":"child-device","name":"Child1","type":"RaspberryPi"}"#) - ]) - .await; + [("c8y/s/us", "101,test-device:custom,Child1,RaspberryPi")], + ) + .await; mqtt.send(MqttMessage::new( &Topic::new_unchecked("te/custom/child1//"), @@ -197,11 +227,9 @@ async fn custom_topic_scheme_registration_mapping() { [( "c8y/s/us", "101,test-device:custom:child1,Child1,RaspberryPi", - ), ( - "te/custom/child1//", r#"{"@id":"test-device:custom:child1","@type":"child-device","name":"Child1","type":"RaspberryPi"}"# )], ) - .await; + .await; // Service with custom scheme mqtt.send(MqttMessage::new( @@ -213,20 +241,21 @@ async fn custom_topic_scheme_registration_mapping() { assert_received_contains_str( &mut mqtt, - [("c8y/s/us", - "102,test-device:custom:service:collectd,systemd,Collectd,up", - ), ( - "te/custom/service/collectd/", - r#"{"@id":"test-device:custom:service:collectd","@type":"service","name":"Collectd","type":"systemd"}"# + [( + "c8y/s/us", + "102,test-device:custom:service:collectd,systemd,Collectd,up", )], ) - .await; + .await; } #[tokio::test] async fn service_registration_mapping() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -248,7 +277,7 @@ async fn service_registration_mapping() { .await .unwrap(); - mqtt.skip(4).await; // Skip mappings of above child device creation messages and republished messages with @id + mqtt.skip(2).await; // Skip mappings of above child device creation messages and republished messages with @id mqtt.send(MqttMessage::new( &Topic::new_unchecked("te/device/main/service/collectd"), @@ -266,8 +295,6 @@ async fn service_registration_mapping() { ) .await; - mqtt.skip(1).await; // Skip republished message with @id - mqtt.send(MqttMessage::new( &Topic::new_unchecked("te/device/child1/service/collectd"), r#"{ "@type": "service", "type": "systemd", "name": "Collectd" }"#, @@ -284,8 +311,6 @@ async fn service_registration_mapping() { ) .await; - mqtt.skip(1).await; // Skip republished message with @id - mqtt.send(MqttMessage::new( &Topic::new_unchecked("te/device/child2/service/collectd"), r#"{ "@type": "service", "type": "systemd", "name": "Collectd" }"#, @@ -301,14 +326,13 @@ async fn service_registration_mapping() { )], ) .await; - - mqtt.skip(1).await; // Skip republished message with @id } #[tokio::test] async fn mapper_publishes_supported_software_types() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -335,8 +359,9 @@ async fn mapper_publishes_supported_software_types() { #[tokio::test] async fn mapper_publishes_advanced_software_list() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -380,8 +405,9 @@ async fn mapper_publishes_advanced_software_list() { async fn mapper_publishes_software_update_request() { // The test assures c8y mapper correctly receives software update request from JSON over MQTT // and converts it to thin-edge json message published on `te/device/main///cmd/software_update/+`. - let cfg_dir = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -441,8 +467,9 @@ async fn mapper_publishes_software_update_status_onto_c8y_topic() { // and publishes status of the operation `501` on `c8y/s/us` // Start SM Mapper - let cfg_dir = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -490,8 +517,9 @@ async fn mapper_publishes_software_update_status_onto_c8y_topic() { #[tokio::test] async fn mapper_publishes_software_update_failed_status_onto_c8y_topic() { // Start SM Mapper - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -541,8 +569,9 @@ async fn mapper_publishes_software_update_request_with_wrong_action() { // Then c8y-mapper publishes an operation status message as failed `502,c8y_SoftwareUpdate,Action remove is not recognized. It must be install or delete.` on `c8/s/us`. // Then the subscriber that subscribed for messages on `c8/s/us` receives these messages and verifies them. - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -588,8 +617,11 @@ async fn mapper_publishes_software_update_request_with_wrong_action() { #[tokio::test] async fn c8y_mapper_alarm_mapping_to_smartrest() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -613,8 +645,11 @@ async fn c8y_mapper_alarm_mapping_to_smartrest() { #[tokio::test] async fn c8y_mapper_child_alarm_mapping_to_smartrest() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -661,8 +696,11 @@ async fn c8y_mapper_child_alarm_mapping_to_smartrest() { #[tokio::test] async fn c8y_mapper_alarm_with_custom_fragment_mapping_to_c8y_json() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -712,8 +750,11 @@ async fn c8y_mapper_alarm_with_custom_fragment_mapping_to_c8y_json() { #[tokio::test] async fn c8y_mapper_child_alarm_with_custom_fragment_mapping_to_c8y_json() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -787,8 +828,11 @@ async fn c8y_mapper_child_alarm_with_custom_fragment_mapping_to_c8y_json() { #[tokio::test] async fn c8y_mapper_alarm_with_message_as_custom_fragment_mapping_to_c8y_json() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -830,8 +874,11 @@ async fn c8y_mapper_alarm_with_message_as_custom_fragment_mapping_to_c8y_json() #[tokio::test] async fn c8y_mapper_child_alarm_with_message_custom_fragment_mapping_to_c8y_json() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -879,8 +926,11 @@ async fn c8y_mapper_child_alarm_with_message_custom_fragment_mapping_to_c8y_json #[tokio::test] async fn c8y_mapper_child_alarm_with_custom_message() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -927,8 +977,11 @@ async fn c8y_mapper_child_alarm_with_custom_message() { #[tokio::test] async fn c8y_mapper_alarm_with_custom_message() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -969,8 +1022,11 @@ async fn c8y_mapper_alarm_with_custom_message() { #[tokio::test] async fn c8y_mapper_child_alarm_empty_payload() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -1000,8 +1056,11 @@ async fn c8y_mapper_child_alarm_empty_payload() { #[tokio::test] async fn c8y_mapper_alarm_empty_payload() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -1022,8 +1081,11 @@ async fn c8y_mapper_alarm_empty_payload() { #[tokio::test] async fn c8y_mapper_alarm_empty_json_payload() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -1050,8 +1112,11 @@ async fn c8y_mapper_alarm_empty_json_payload() { #[tokio::test] async fn c8y_mapper_child_event() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -1112,8 +1177,11 @@ async fn c8y_mapper_child_event() { #[tokio::test] async fn c8y_mapper_child_service_event() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -1197,8 +1265,11 @@ async fn c8y_mapper_child_service_event() { #[tokio::test] async fn c8y_mapper_main_service_event() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -1264,8 +1335,11 @@ async fn c8y_mapper_main_service_event() { #[tokio::test] async fn c8y_mapper_child_service_alarm() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -1342,8 +1416,11 @@ async fn c8y_mapper_child_service_alarm() { #[tokio::test] async fn c8y_mapper_main_service_alarm() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -1402,8 +1479,11 @@ async fn c8y_mapper_main_service_alarm() { #[tokio::test] async fn c8y_mapper_alarm_complex_text_fragment_in_payload_failed() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -1435,8 +1515,9 @@ async fn c8y_mapper_alarm_complex_text_fragment_in_payload_failed() { #[tokio::test] async fn mapper_handles_multiple_modules_in_update_list_sm_requests() { // The test assures if Mapper can handle multiple update modules received via JSON over MQTT - let cfg_dir = TempTedgeDir::new(); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); @@ -1506,10 +1587,11 @@ async fn mapper_publishes_supported_operations() { // The test assures tede-mapper reads/parses the operations from operations directory and // correctly publishes the supported operations message on `c8y/s/us` // and verifies the supported operations that are published by the tedge-mapper. - let cfg_dir = TempTedgeDir::new(); - create_thin_edge_operations(&cfg_dir, vec!["c8y_TestOp1", "c8y_TestOp2"]); + let ttd = TempTedgeDir::new(); + create_thin_edge_operations(&ttd, vec!["c8y_TestOp1", "c8y_TestOp2"]); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, false).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); mqtt.skip(2).await; @@ -1524,10 +1606,11 @@ async fn mapper_dynamically_updates_supported_operations_for_tedge_device() { // correctly publishes them on to `c8y/s/us`. // When mapper is running test adds a new operation into the operations directory, then the mapper discovers the new // operation and publishes list of supported operation including the new operation, and verifies the device create message. - let cfg_dir = TempTedgeDir::new(); - create_thin_edge_operations(&cfg_dir, vec!["c8y_TestOp1", "c8y_TestOp2"]); + let ttd = TempTedgeDir::new(); + create_thin_edge_operations(&ttd, vec!["c8y_TestOp1", "c8y_TestOp2"]); - let (mqtt, _http, mut fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, false).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, mut fs, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -1547,8 +1630,7 @@ async fn mapper_dynamically_updates_supported_operations_for_tedge_device() { // Simulate FsEvent for the creation of a new operation file fs.send(FsWatchEvent::FileCreated( - cfg_dir - .dir("operations") + ttd.dir("operations") .dir("c8y") .file("c8y_TestOp3") .to_path_buf(), @@ -1603,22 +1685,22 @@ async fn mapper_dynamically_updates_supported_operations_for_child_device() { // The test assures tedge-mapper reads the operations for the child devices from the operations directory, and then it publishes them on to `c8y/s/us/child1`. // When mapper is running test adds a new operation for a child into the operations directory, then the mapper discovers the new // operation and publishes list of supported operation for the child device including the new operation, and verifies the device create message. - let cfg_dir = TempTedgeDir::new(); + let ttd = TempTedgeDir::new(); create_thin_edge_child_operations( - &cfg_dir, + &ttd, "test-device:device:child1", vec!["c8y_ChildTestOp1", "c8y_ChildTestOp2"], ); - let (mqtt, _http, mut fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, false).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, mut fs, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; // Add a new operation for the child device // Simulate FsEvent for the creation of a new operation file fs.send(FsWatchEvent::FileCreated( - cfg_dir - .dir("operations") + ttd.dir("operations") .dir("c8y") .dir("test-device:device:child1") .file("c8y_ChildTestOp3") @@ -1676,14 +1758,15 @@ async fn mapper_dynamically_updates_supported_operations_for_nested_child_device // The test assures tedge-mapper reads the operations for the child devices from the operations directory, and then it publishes them on to `c8y/s/us/child1`. // When mapper is running test adds a new operation for a child into the operations directory, then the mapper discovers the new // operation and publishes list of supported operation for the child device including the new operation, and verifies the device create message. - let cfg_dir = TempTedgeDir::new(); + let ttd = TempTedgeDir::new(); create_thin_edge_child_operations( - &cfg_dir, + &ttd, "child11", vec!["c8y_ChildTestOp1", "c8y_ChildTestOp2"], ); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, false).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); skip_init_messages(&mut mqtt).await; @@ -1754,7 +1837,7 @@ async fn mapper_updating_the_inventory_fragments_from_file() { // The test Creates an inventory file in (Temp_base_Dir)/device/inventory.json // The tedge-mapper parses the inventory fragment file and publishes on c8y/inventory/managedObjects/update/test-device // Verify the fragment message that is published - let cfg_dir = TempTedgeDir::new(); + let ttd = TempTedgeDir::new(); let version = env!("CARGO_PKG_VERSION"); let custom_fragment_content = json!({ @@ -1772,9 +1855,10 @@ async fn mapper_updating_the_inventory_fragments_from_file() { "version": "1.20140107-1" } }); - create_inventroy_json_file_with_content(&cfg_dir, &custom_fragment_content.to_string()); + create_inventroy_json_file_with_content(&ttd, &custom_fragment_content.to_string()); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); assert_received_includes_json( @@ -1813,7 +1897,7 @@ async fn forbidden_keys_in_inventory_fragments_file_ignored() { // The test Creates an inventory file in (Temp_base_Dir)/device/inventory.json // The tedge-mapper parses the inventory fragment file and publishes on c8y/inventory/managedObjects/update/test-device // Verify the fragment message that is published - let cfg_dir = TempTedgeDir::new(); + let ttd = TempTedgeDir::new(); let version = env!("CARGO_PKG_VERSION"); let custom_fragment_content = json!({ @@ -1825,9 +1909,10 @@ async fn forbidden_keys_in_inventory_fragments_file_ignored() { "version": "1.20140107-1" } }); - create_inventroy_json_file_with_content(&cfg_dir, &custom_fragment_content.to_string()); + create_inventroy_json_file_with_content(&ttd, &custom_fragment_content.to_string()); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); assert_received_includes_json( @@ -1869,11 +1954,11 @@ async fn custom_operation_without_timeout_successful() { // The test assures SM Mapper correctly receives custom operation on `c8y/s/ds` // and executes the custom operation successfully, no timeout given here. - let cfg_dir = TempTedgeDir::new(); + let ttd = TempTedgeDir::new(); - let cmd_file = cfg_dir.path().join("command"); + let cmd_file = ttd.path().join("command"); //create custom operation file - create_custom_op_file(&cfg_dir, cmd_file.as_path(), None, None); + create_custom_op_file(&ttd, cmd_file.as_path(), None, None); //create command let content = r#"#!/bin/sh for i in $(seq 1 2) @@ -1884,7 +1969,8 @@ async fn custom_operation_without_timeout_successful() { "#; create_custom_cmd(cmd_file.as_path(), content); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -1923,7 +2009,7 @@ Executed successfully without timeout EOF "; - assert_command_exec_log_content(cfg_dir, expected_content); + assert_command_exec_log_content(ttd, expected_content); } #[tokio::test] @@ -1931,10 +2017,10 @@ async fn custom_operation_with_timeout_successful() { // The test assures SM Mapper correctly receives custom operation on `c8y/s/ds` // and executes the custom operation within the timeout period - let cfg_dir = TempTedgeDir::new(); - let cmd_file = cfg_dir.path().join("command"); + let ttd = TempTedgeDir::new(); + let cmd_file = ttd.path().join("command"); //create custom operation file - create_custom_op_file(&cfg_dir, cmd_file.as_path(), Some(4), Some(2)); + create_custom_op_file(&ttd, cmd_file.as_path(), Some(4), Some(2)); //create command let content = r#"#!/bin/sh for i in $(seq 1 2) @@ -1945,7 +2031,8 @@ async fn custom_operation_with_timeout_successful() { "#; create_custom_cmd(cmd_file.as_path(), content); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -1981,7 +2068,7 @@ Successfully Executed EOF "; - assert_command_exec_log_content(cfg_dir, expected_content); + assert_command_exec_log_content(ttd, expected_content); } #[tokio::test] @@ -1990,10 +2077,10 @@ async fn custom_operation_timeout_sigterm() { // and executes the custom operation, it will timeout because it will not complete before given timeout // sigterm is sent to stop the custom operation - let cfg_dir = TempTedgeDir::new(); - let cmd_file = cfg_dir.path().join("command"); + let ttd = TempTedgeDir::new(); + let cmd_file = ttd.path().join("command"); //create custom operation file - create_custom_op_file(&cfg_dir, cmd_file.as_path(), Some(1), Some(2)); + create_custom_op_file(&ttd, cmd_file.as_path(), Some(1), Some(2)); //create command let content = r#"#!/bin/sh trap 'echo received SIGTERM; exit 124' TERM @@ -2005,7 +2092,8 @@ async fn custom_operation_timeout_sigterm() { "#; create_custom_cmd(cmd_file.as_path(), content); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -2046,7 +2134,7 @@ received SIGTERM EOF "; - assert_command_exec_log_content(cfg_dir, expected_content); + assert_command_exec_log_content(ttd, expected_content); } #[tokio::test] @@ -2055,11 +2143,11 @@ async fn custom_operation_timeout_sigkill() { // and executes the custom operation, it will timeout because it will not complete before given timeout // sigterm sent first, still the operation did not stop, so sigkill will be sent to stop the operation - let cfg_dir = TempTedgeDir::new(); + let ttd = TempTedgeDir::new(); - let cmd_file = cfg_dir.path().join("command"); + let cmd_file = ttd.path().join("command"); //create custom operation file - create_custom_op_file(&cfg_dir, cmd_file.as_path(), Some(1), Some(2)); + create_custom_op_file(&ttd, cmd_file.as_path(), Some(1), Some(2)); //create command let content = r#"#!/bin/sh trap 'echo ignore SIGTERM' TERM @@ -2071,7 +2159,8 @@ async fn custom_operation_timeout_sigkill() { "#; create_custom_cmd(cmd_file.as_path(), content); - let (mqtt, http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, http, .. } = test_handle; spawn_dummy_c8y_http_proxy(http); let mut mqtt = mqtt.with_timeout(Duration::from_secs(5)); @@ -2113,7 +2202,7 @@ main 2 EOF "; - assert_command_exec_log_content(cfg_dir, expected_content); + assert_command_exec_log_content(ttd, expected_content); } /// This test aims to verify that when a telemetry message is emitted from an @@ -2124,8 +2213,9 @@ EOF /// shall be emitted by the mapper. #[tokio::test] async fn inventory_registers_unknown_entity_once() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, _timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { mqtt, .. } = test_handle; let mut mqtt = mqtt.with_timeout(TEST_TIMEOUT_MS); @@ -2169,8 +2259,11 @@ async fn inventory_registers_unknown_entity_once() { #[tokio::test] async fn c8y_mapper_nested_child_alarm_mapping_to_smartrest() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -2230,8 +2323,11 @@ async fn c8y_mapper_nested_child_alarm_mapping_to_smartrest() { #[tokio::test] async fn c8y_mapper_nested_child_event_mapping_to_smartrest() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -2300,8 +2396,11 @@ async fn c8y_mapper_nested_child_event_mapping_to_smartrest() { #[tokio::test] async fn c8y_mapper_nested_child_service_alarm_mapping_to_smartrest() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -2365,8 +2464,11 @@ async fn c8y_mapper_nested_child_service_alarm_mapping_to_smartrest() { #[tokio::test] async fn c8y_mapper_nested_child_service_event_mapping_to_smartrest() { - let cfg_dir = TempTedgeDir::new(); - let (mqtt, _http, _fs, mut timer, _ul, _dl) = spawn_c8y_mapper_actor(&cfg_dir, true).await; + let ttd = TempTedgeDir::new(); + let test_handle = spawn_c8y_mapper_actor(&ttd, true).await; + let TestHandle { + mqtt, mut timer, .. + } = test_handle; // Complete sync phase so that alarm mapping starts trigger_timeout(&mut timer).await; @@ -2433,8 +2535,8 @@ async fn c8y_mapper_nested_child_service_event_mapping_to_smartrest() { .await; } -fn assert_command_exec_log_content(cfg_dir: TempTedgeDir, expected_contents: &str) { - let paths = fs::read_dir(cfg_dir.to_path_buf().join("agent")).unwrap(); +fn assert_command_exec_log_content(ttd: TempTedgeDir, expected_contents: &str) { + let paths = fs::read_dir(ttd.to_path_buf().join("agent")).unwrap(); for path in paths { let mut file = File::open(path.unwrap().path()).expect("Unable to open the command exec log file"); @@ -2446,12 +2548,12 @@ fn assert_command_exec_log_content(cfg_dir: TempTedgeDir, expected_contents: &st } fn create_custom_op_file( - cfg_dir: &TempTedgeDir, + ttd: &TempTedgeDir, cmd_file: &Path, graceful_timeout: Option, forceful_timeout: Option, ) { - let custom_op_file = cfg_dir.dir("operations").dir("c8y").file("c8y_Command"); + let custom_op_file = ttd.dir("operations").dir("c8y").file("c8y_Command"); let mut custom_content = toml::map::Map::new(); custom_content.insert("name".into(), toml::Value::String("c8y_Command".into())); custom_content.insert("topic".into(), toml::Value::String("c8y/s/ds".into())); @@ -2476,21 +2578,21 @@ fn create_custom_cmd(custom_cmd: &Path, content: &str) { with_exec_permission(custom_cmd, content); } -fn create_inventroy_json_file_with_content(cfg_dir: &TempTedgeDir, content: &str) { - let file = cfg_dir.dir("device").file("inventory.json"); +fn create_inventroy_json_file_with_content(ttd: &TempTedgeDir, content: &str) { + let file = ttd.dir("device").file("inventory.json"); file.with_raw_content(content); } -fn create_thin_edge_operations(cfg_dir: &TempTedgeDir, ops: Vec<&str>) { - let p1 = cfg_dir.dir("operations"); +fn create_thin_edge_operations(ttd: &TempTedgeDir, ops: Vec<&str>) { + let p1 = ttd.dir("operations"); let tedge_ops_dir = p1.dir("c8y"); for op in ops { tedge_ops_dir.file(op); } } -fn create_thin_edge_child_operations(cfg_dir: &TempTedgeDir, child_id: &str, ops: Vec<&str>) { - let p1 = cfg_dir.dir("operations"); +fn create_thin_edge_child_operations(ttd: &TempTedgeDir, child_id: &str, ops: Vec<&str>) { + let p1 = ttd.dir("operations"); let tedge_ops_dir = p1.dir("c8y"); let child_ops_dir = tedge_ops_dir.dir(child_id); for op in ops { @@ -2503,36 +2605,18 @@ async fn trigger_timeout(timer: &mut FakeServerBox) { timer.send(Timeout::new(())).await.unwrap(); } -pub(crate) async fn spawn_c8y_mapper_actor( - tmp_dir: &TempTedgeDir, - init: bool, -) -> ( - SimpleMessageBox, - FakeServerBox, - SimpleMessageBox, - FakeServerBox, - FakeServerBox, - FakeServerBox, -) { - let handle = - spawn_c8y_mapper_actor_with_config(tmp_dir, test_mapper_config(tmp_dir), init).await; - ( - handle.mqtt_box, - handle.c8y_http_box, - handle.fs_box, - handle.timer_box, - handle.ul_box, - handle.dl_box, - ) +pub(crate) async fn spawn_c8y_mapper_actor(tmp_dir: &TempTedgeDir, init: bool) -> TestHandle { + spawn_c8y_mapper_actor_with_config(tmp_dir, test_mapper_config(tmp_dir), init).await } pub(crate) struct TestHandle { - pub mqtt_box: SimpleMessageBox, - pub c8y_http_box: FakeServerBox, - pub fs_box: SimpleMessageBox, - pub timer_box: FakeServerBox, - pub ul_box: FakeServerBox, - pub dl_box: FakeServerBox, + pub mqtt: SimpleMessageBox, + pub http: FakeServerBox, + pub fs: SimpleMessageBox, + pub timer: FakeServerBox, + pub ul: FakeServerBox, + pub dl: FakeServerBox, + pub avail: SimpleMessageBox, } pub(crate) async fn spawn_c8y_mapper_actor_with_config( @@ -2561,7 +2645,7 @@ pub(crate) async fn spawn_c8y_mapper_actor_with_config( SimpleMessageBoxBuilder::new("ServiceMonitor", 1); let bridge_health_topic = config.bridge_health_topic.clone(); - let c8y_mapper_builder = C8yMapperBuilder::try_new( + let mut c8y_mapper_builder = C8yMapperBuilder::try_new( config, &mut mqtt_builder, &mut c8y_proxy_builder, @@ -2573,6 +2657,12 @@ pub(crate) async fn spawn_c8y_mapper_actor_with_config( ) .unwrap(); + let mut availability_box_builder: SimpleMessageBoxBuilder = + SimpleMessageBoxBuilder::new("Availability", 10); + availability_box_builder + .connect_source(AvailabilityBuilder::channels(), &mut c8y_mapper_builder); + c8y_mapper_builder.connect_source(NoConfig, &mut availability_box_builder); + let actor = c8y_mapper_builder.build(); tokio::spawn(async move { actor.run().await }); @@ -2581,12 +2671,13 @@ pub(crate) async fn spawn_c8y_mapper_actor_with_config( service_monitor_box.send(bridge_status_msg).await.unwrap(); TestHandle { - mqtt_box: mqtt_builder.build(), - c8y_http_box: c8y_proxy_builder.build(), - fs_box: fs_watcher_builder.build(), - timer_box: timer_builder.build(), - ul_box: uploader_builder.build(), - dl_box: downloader_builder.build(), + mqtt: mqtt_builder.build(), + http: c8y_proxy_builder.build(), + fs: fs_watcher_builder.build(), + timer: timer_builder.build(), + ul: uploader_builder.build(), + dl: downloader_builder.build(), + avail: availability_box_builder.build(), } } diff --git a/crates/extensions/tedge_mqtt_ext/src/test_helpers.rs b/crates/extensions/tedge_mqtt_ext/src/test_helpers.rs index ddf592a7b29..482aac48433 100644 --- a/crates/extensions/tedge_mqtt_ext/src/test_helpers.rs +++ b/crates/extensions/tedge_mqtt_ext/src/test_helpers.rs @@ -4,10 +4,11 @@ use mqtt_channel::TopicFilter; use std::fmt::Debug; use tedge_actors::MessageReceiver; -pub async fn assert_received_contains_str<'a, I>( - messages: &mut dyn MessageReceiver, +pub async fn assert_received_contains_str<'a, M, I>( + messages: &mut dyn MessageReceiver, expected: I, ) where + M: Into, I: IntoIterator, { for expected_msg in expected.into_iter() { @@ -18,20 +19,21 @@ pub async fn assert_received_contains_str<'a, I>( expected_msg ); let message = message.unwrap(); - assert_message_contains_str(&message, expected_msg); + assert_message_contains_str(&message.into(), expected_msg); } } -pub async fn assert_received_includes_json( - messages: &mut dyn MessageReceiver, +pub async fn assert_received_includes_json( + messages: &mut dyn MessageReceiver, expected: I, ) where + M: Into, I: IntoIterator, S: AsRef, { for expected_msg in expected.into_iter() { let message = messages.recv().await.expect("MQTT channel closed"); - assert_message_includes_json(&message, expected_msg); + assert_message_includes_json(&message.into(), expected_msg); } } diff --git a/docs/src/operate/c8y/device-availability.md b/docs/src/operate/c8y/device-availability.md new file mode 100644 index 00000000000..551bfeb4130 --- /dev/null +++ b/docs/src/operate/c8y/device-availability.md @@ -0,0 +1,64 @@ +--- +title: Availability Monitoring +tags: [Operate, Cumulocity, Monitoring] +description: Monitoring the availability of devices +--- + +# Availability Monitoring + +%%te%% fully supports the [Cumulocity IoT's device availability monitoring feature](https://cumulocity.com/docs/device-management-application/monitoring-and-controlling-devices/#availability) +allowing you to set the desired required interval for the devices +and also sending heartbeats to **Cumulocity IoT** periodically when a device is deemed available. +%%te%% considers a device as available when the `tedge-agent` service on it is up and running, +monitored using its service health endpoint. +The health endpoint can be changed from the `tedge-agent` to any other entity's health endpoint as well. + +## Set the required availability interval + +As described in the [Cumulocity IoT's user documentation](https://cumulocity.com/docs/device-integration/fragment-library/#device-availability), +%%te%% main and child devices set their required interval during their first connection. +Availability monitoring is enabled by default with a default required interval of 1 hour. +The value to be updated using the `tedge config set` command as follows: + +```sh +sudo tedge config set c8y.availability.interval 30m +``` + +If the value is set to less than 1 minute or 0, +availability monitoring is disabled and the device is considered to be in maintenance mode. + +## Change the health endpoint for heartbeat messages + +Once the device connection to **Cumulocity IoT** is established, +**tedge-mapper-c8y** will keep sending heartbeat messages (empty inventory update messages) on behalf of the main and child devices +to keep their availability active. +By default, the status of the **tedge-agent** service is used to determine whether the device is available or not. + +For example, the device `device/my-device//` is considered "available" when its tedge-agent service status is reported as `up` as shown below: + +```sh te2mqtt formats=v1 +tedge mqtt pub te/device/my-device/service/tedge-agent/status/health '{"status":"up"}' -q 2 -r +``` + +To change the health endpoint from the default to a custom value, include the `@health` property in the entity registration message. +The `@health` value should be a valid [entity topic identifier](../../contribute/design/mqtt-topic-design.md). + +```sh te2mqtt formats=v1 +tedge mqtt pub te/device/my-device// '{"@health":"device/my-device/service/foo", "@type":"child-device"}' -q 2 -r +``` + +If the status of the new endpoint is reported as `up`, the device is considered "available", +and a heartbeat signal is sent to Cumulocity IoT. +If the status has any other value, the device is considered "unavailable", +and no heartbeat message are sent to Cumulocity until the status changes to `up` again. + +## Disable the availability monitoring + +By default, the feature is enabled. +To disable the feature, use the `tedge config set` command as follows and restart the **tedge-mapper-c8y** service. + +```sh +sudo tedge config set c8y.availability.enable false +``` + +When disabled, the required availability interval and periodic heartbeat messages aren't sent to Cumulocity IoT. diff --git a/tests/RobotFramework/requirements/requirements.txt b/tests/RobotFramework/requirements/requirements.txt index 5d9c3663fbc..534afd57701 100644 --- a/tests/RobotFramework/requirements/requirements.txt +++ b/tests/RobotFramework/requirements/requirements.txt @@ -2,7 +2,7 @@ dateparser~=1.2.0 paho-mqtt~=1.6.1 python-dotenv~=1.0.0 robotframework~=7.0.0 -robotframework-c8y @ git+https://github.com/reubenmiller/robotframework-c8y.git@0.34.0 +robotframework-c8y @ git+https://github.com/reubenmiller/robotframework-c8y.git@0.34.1 robotframework-debuglibrary~=2.5.0 robotframework-jsonlibrary~=0.5 robotframework-pabot~=2.18.0 diff --git a/tests/RobotFramework/tests/cumulocity/availability/c8y_required_availability.robot b/tests/RobotFramework/tests/cumulocity/availability/c8y_required_availability.robot new file mode 100644 index 00000000000..7489773a29f --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/availability/c8y_required_availability.robot @@ -0,0 +1,64 @@ +*** Settings *** +Resource ../../../resources/common.resource +Library Cumulocity +Library ThinEdgeIO + +Test Tags theme:c8y theme:monitoring +Test Setup Test Setup +Test Teardown Get Logs + +*** Test Cases *** + +c8y_RequiredAvailability is set by default to an hour + Execute Command ./bootstrap.sh + + # Main + Device Should Exist ${DEVICE_SN} + Device Should Have Fragment Values c8y_RequiredAvailability.responseInterval\=60 + + # Child + Register child + Device Should Have Fragment Values c8y_RequiredAvailability.responseInterval\=60 + +c8y_RequiredAvailability is set with custom value + # Set tedge config value before connecting + Execute Command ./bootstrap.sh --no-bootstrap --no-connect + Execute Command sudo tedge config set c8y.availability.interval 0 + Execute Command ./bootstrap.sh --no-install + + # Main + Device Should Exist ${DEVICE_SN} + Device Should Have Fragment Values c8y_RequiredAvailability.responseInterval\=0 + + # Child + Register child + Device Should Have Fragment Values c8y_RequiredAvailability.responseInterval\=0 + +c8y_RequiredAvailability is not set when disabled + # Set tedge config value before connecting + Execute Command ./bootstrap.sh --no-bootstrap --no-connect + Execute Command sudo tedge config set c8y.availability.enable false + Execute Command ./bootstrap.sh --no-install + + # Main + Device Should Exist ${DEVICE_SN} + Managed Object Should Not Have Fragments c8y_RequiredAvailability + + # Child + Register child + Managed Object Should Not Have Fragments c8y_RequiredAvailability + +*** Keywords *** +Test Setup + ${DEVICE_SN}= Setup skip_bootstrap=True + Set Test Variable $DEVICE_SN + + ${CHILD_SN}= Get Random Name + Set Test Variable $CHILD_SN + Set Test Variable $CHILD_XID ${DEVICE_SN}:device:${CHILD_SN} + +Register child + [Arguments] + Execute Command tedge mqtt pub --retain 'te/device/${CHILD_SN}//' '{"@type":"child-device"}' + Set Device ${CHILD_XID} + Device Should Exist ${CHILD_XID} diff --git a/tests/RobotFramework/tests/cumulocity/availability/heartbeat.robot b/tests/RobotFramework/tests/cumulocity/availability/heartbeat.robot new file mode 100644 index 00000000000..1d28e07b851 --- /dev/null +++ b/tests/RobotFramework/tests/cumulocity/availability/heartbeat.robot @@ -0,0 +1,101 @@ +*** Settings *** +Resource ../../../resources/common.resource +Library Cumulocity +Library ThinEdgeIO + +Test Tags theme:c8y theme:monitoring +Test Setup Test Setup +Test Teardown Get Logs + + +*** Variables *** + +${INTERVAL_CHANGE_TIMEOUT} 180 +${CHECK_INTERVAL} 10 +${HEARTBEAT_INTERVAL} 60 + +*** Test Cases *** + +### Main Device ### +Heartbeat is sent + [Documentation] Full end-to-end test which will check the Cumulocity IoT behaviour to sending the heartbeat signal + ... The tests therefore relies on the backend availability service which then sets the c8y_Availability fragment + ... based on the last received telemetry data. + Device Should Have Fragment Values c8y_Availability.status\=AVAILABLE timeout=${INTERVAL_CHANGE_TIMEOUT} wait=${CHECK_INTERVAL} + Stop Service tedge-agent + Service Health Status Should Be Down tedge-agent + Device Should Have Fragment Values c8y_Availability.status\=UNAVAILABLE timeout=${INTERVAL_CHANGE_TIMEOUT} wait=${CHECK_INTERVAL} + + Start Service tedge-agent + Service Health Status Should Be Up tedge-agent + Device Should Have Fragment Values c8y_Availability.status\=AVAILABLE timeout=${INTERVAL_CHANGE_TIMEOUT} wait=${CHECK_INTERVAL} + +# +# Note about remaining test cases +# The remaining test cases do not use the platform to assert whether the availability is set or not +# as this either takes too long, and is too flakey as the performance of the backend service which sets +# the c8y_Availability fragments varies greatly (as it is designed for longer intervals, e.g. ~30/60 mins) +# Instead, the test cases check if the heartbeat signal is being sent for the given devices which does not +# rely on any additional platform checks. +# + +Heartbeat is sent based on the custom health topic status + Execute Command tedge mqtt pub --retain 'te/device/main//' '{"@type":"device","@health":"device/main/service/foo"}' + Execute Command tedge mqtt pub --retain 'te/device/main/service/foo/status/health' '{"status":"up"}' + ${existing_count}= Should Have Heartbeat Message Count ${DEVICE_SN} minimum=1 + + # Stop tedge-agent to make sure the heartbeat is not sent based on the tedge-agent status + Stop Service tedge-agent + Service Health Status Should Be Down tedge-agent + ${existing_count}= Should Have Heartbeat Message Count ${DEVICE_SN} minimum=${existing_count + 1} timeout=${HEARTBEAT_INTERVAL} + + +### Child Device ### +Child heartbeat is sent + # Register a child device + Execute Command tedge mqtt pub --retain 'te/device/${CHILD_SN}//' '{"@type":"child-device"}' + Set Device ${CHILD_XID} + Device Should Exist ${CHILD_XID} + + ${existing_count}= Should Have Heartbeat Message Count ${CHILD_XID} minimum=0 maximum=0 + + # Fake tedge-agent status is up for the child device + Execute Command tedge mqtt pub --retain 'te/device/${CHILD_SN}/service/tedge-agent/status/health' '{"status":"up"}' + Should Have Heartbeat Message Count ${CHILD_XID} minimum=${existing_count + 1} timeout=${HEARTBEAT_INTERVAL} + +Child heartbeat is sent based on the custom health topic status + # Register a child device + Execute Command tedge mqtt pub --retain 'te/device/${CHILD_SN}//' '{"@type":"child-device", "@health":"device/${CHILD_SN}/service/bar"}' + Set Device ${CHILD_XID} + Device Should Exist ${CHILD_XID} + + ${existing_count}= Should Have Heartbeat Message Count ${CHILD_XID} minimum=0 maximum=0 + + # The custom health endpoint is up but tedge-agent is down + Execute Command tedge mqtt pub --retain 'te/device/${CHILD_SN}/service/bar/status/health' '{"status":"up"}' + Execute Command tedge mqtt pub --retain 'te/device/${CHILD_SN}/service/tedge-agent/status/health' '{"status":"down"}' + + Should Have Heartbeat Message Count ${CHILD_XID} minimum=${existing_count + 1} timeout=${HEARTBEAT_INTERVAL} + + +*** Keywords *** +Test Setup + ${DEVICE_SN}= Setup skip_bootstrap=True + Set Test Variable $DEVICE_SN + + ${CHILD_SN}= Get Random Name + Set Test Variable $CHILD_SN + Set Test Variable $CHILD_XID ${DEVICE_SN}:device:${CHILD_SN} + + # Set tedge config value before connecting + Execute Command ./bootstrap.sh --no-bootstrap --no-connect + Execute Command sudo tedge config set c8y.availability.interval 1m + Execute Command ./bootstrap.sh --no-install + + Device Should Exist ${DEVICE_SN} + +Should Have Heartbeat Message Count + [Arguments] ${SERIAL} ${minimum}=${None} ${maximum}=${None} ${timeout}=30 + ${messages}= Should Have MQTT Messages c8y/inventory/managedObjects/update/${SERIAL} minimum=${minimum} maximum=${maximum} message_pattern=^\{\}$ timeout=${timeout} + ${count}= Get Length ${messages} + RETURN ${count} diff --git a/tests/RobotFramework/tests/cumulocity/registration/registration_lifecycle.robot b/tests/RobotFramework/tests/cumulocity/registration/registration_lifecycle.robot index f41ec4a7812..81e981f32ad 100644 --- a/tests/RobotFramework/tests/cumulocity/registration/registration_lifecycle.robot +++ b/tests/RobotFramework/tests/cumulocity/registration/registration_lifecycle.robot @@ -4,8 +4,7 @@ Library Cumulocity Library ThinEdgeIO Test Tags theme:c8y theme:registration theme:deregistration -Suite Setup Custom Setup -Test Setup Test Setup +Test Setup Custom Setup Test Teardown Get Logs ${DEVICE_SN} *** Test Cases *** @@ -144,29 +143,27 @@ Register devices using custom MQTT schema Register tedge-agent when tedge-mapper-c8y is not running #2389 - [Teardown] Start Service tedge-mapper-c8y - Device Should Exist ${DEVICE_SN} - Stop Service tedge-mapper-c8y Execute Command cmd=timeout 5 env TEDGE_RUN_LOCK_FILES=false tedge-agent --mqtt-device-topic-id device/offlinechild1// ignore_exit_code=${True} Start Service tedge-mapper-c8y + Service Health Status Should Be Up tedge-mapper-c8y - Should Be A Child Device Of Device ${DEVICE_SN}:device:offlinechild1 Should Have MQTT Messages te/device/offlinechild1// minimum=1 + Cumulocity.Set Managed Object ${DEVICE_SN} + Should Be A Child Device Of Device ${DEVICE_SN}:device:offlinechild1 Device Should Exist ${DEVICE_SN}:device:offlinechild1 Cumulocity.Restart Device Should Have MQTT Messages te/device/offlinechild1///cmd/restart/+ Early data messages cached and processed - [Teardown] Re-enable auto-registration and collect logs ${timestamp}= Get Unix Timestamp ${prefix}= Get Random Name Execute Command sudo tedge config set c8y.entity_store.auto_register false Restart Service tedge-mapper-c8y Service Health Status Should Be Up tedge-mapper-c8y - ${children}= Create List child0 child00 child01 child000 child0000 child00000 + ${children}= Create List child0 child00 child01 child02 child000 child0000 child00000 FOR ${child} IN @{children} Execute Command sudo tedge mqtt pub 'te/device/${child}///m/environment' '{ "temp": 50 }' Execute Command sudo tedge mqtt pub 'te/device/${child}///twin/maintenance_mode' 'true' @@ -177,56 +174,41 @@ Early data messages cached and processed Execute Command tedge mqtt pub --retain 'te/device/child0000//' '{"@type":"child-device","@id":"${prefix}child0000","@parent": "device/child000//"}' Execute Command tedge mqtt pub --retain 'te/device/child01//' '{"@type":"child-device","@id":"${prefix}child01","@parent": "device/child0//"}' Execute Command tedge mqtt pub --retain 'te/device/child00//' '{"@type":"child-device","@id":"${prefix}child00","@parent": "device/child0//"}' + Execute Command tedge mqtt pub --retain 'te/device/child02//' '{"@type":"child-device","@parent": "device/child0//"}' Execute Command tedge mqtt pub --retain 'te/device/child0//' '{"@type":"child-device","@id":"${prefix}child0"}' - FOR ${child} IN @{children} - Cumulocity.Set Device ${prefix}${child} + Check Child Device ${DEVICE_SN} ${prefix}child0 ${prefix}child0 thin-edge.io-child + Check Child Device ${prefix}child0 ${prefix}child00 ${prefix}child00 thin-edge.io-child + Check Child Device ${prefix}child0 ${prefix}child01 ${prefix}child01 thin-edge.io-child + Check Child Device ${prefix}child0 ${DEVICE_SN}:device:child02 ${DEVICE_SN}:device:child02 thin-edge.io-child + Check Child Device ${prefix}child00 ${prefix}child000 ${prefix}child000 thin-edge.io-child + Check Child Device ${prefix}child000 ${prefix}child0000 ${prefix}child0000 thin-edge.io-child + Check Child Device ${prefix}child0000 ${prefix}child00000 ${prefix}child00000 thin-edge.io-child + + ${xids}= Create List ${prefix}child0 ${prefix}child00 ${prefix}child01 ${DEVICE_SN}:device:child02 ${prefix}child000 ${prefix}child0000 ${prefix}child00000 + FOR ${xid} IN @{xids} + Cumulocity.Set Device ${xid} Device Should Have Measurements type=environment minimum=1 maximum=1 Device Should Have Fragments maintenance_mode END +Entities persisted and restored + Execute Command sudo tedge config set c8y.entity_store.clean_start false Restart Service tedge-mapper-c8y Service Health Status Should Be Up tedge-mapper-c8y -Early data messages cached and processed without @id in registration messages - [Teardown] Re-enable auto-registration and collect logs - ${timestamp}= Get Unix Timestamp ${prefix}= Get Random Name - Execute Command sudo tedge config set c8y.entity_store.auto_register false - Restart Service tedge-mapper-c8y - Service Health Status Should Be Up tedge-mapper-c8y - ${children}= Create List child0 child00 child01 child000 child0000 child00000 - FOR ${child} IN @{children} - Execute Command sudo tedge mqtt pub 'te/device/${prefix}${child}///m/environment' '{ "temp": 50 }' - Execute Command sudo tedge mqtt pub 'te/device/${prefix}${child}///twin/maintenance_mode' 'true' - END - - Execute Command tedge mqtt pub --retain 'te/device/${prefix}child000//' '{"@type":"child-device","@parent": "device/${prefix}child00//"}' - Execute Command tedge mqtt pub --retain 'te/device/${prefix}child00000//' '{"@type":"child-device","@parent": "device/${prefix}child0000//"}' - Execute Command tedge mqtt pub --retain 'te/device/${prefix}child0000//' '{"@type":"child-device","@parent": "device/${prefix}child000//"}' - Execute Command tedge mqtt pub --retain 'te/device/${prefix}child01//' '{"@type":"child-device","@parent": "device/${prefix}child0//"}' - Execute Command tedge mqtt pub --retain 'te/device/${prefix}child00//' '{"@type":"child-device","@parent": "device/${prefix}child0//"}' - Execute Command tedge mqtt pub --retain 'te/device/${prefix}child0//' '{"@type":"child-device"}' - - FOR ${child} IN @{children} - Cumulocity.Set Device ${DEVICE_SN}:device:${prefix}${child} - Device Should Have Measurements type=environment minimum=1 maximum=1 - Device Should Have Fragments maintenance_mode - Should Have MQTT Messages te/device/${prefix}${child}// message_contains="@id":"${DEVICE_SN}:device:${prefix}${child}" message_contains="@type":"child-device" - END + # without @id + Execute Command tedge mqtt pub --retain 'te/school/shop/plc1/' '{"@type":"child-device"}' + Execute Command tedge mqtt pub --retain 'te/school/shop/plc1/sensor1' '{"@type":"child-device","@parent":"school/shop/plc1/"}' + Execute Command tedge mqtt pub --retain 'te/school/shop/plc1/metrics' '{"@type":"service","@parent":"school/shop/plc1/"}' - Restart Service tedge-mapper-c8y - Service Health Status Should Be Up tedge-mapper-c8y + External Identity Should Exist ${DEVICE_SN}:school:shop:plc1 + External Identity Should Exist ${DEVICE_SN}:school:shop:plc1:sensor1 + External Identity Should Exist ${DEVICE_SN}:school:shop:plc1:metrics -Entities persisted and restored - [Teardown] Enable clean start and collect logs - Execute Command sudo tedge config set c8y.entity_store.clean_start false - Restart Service tedge-mapper-c8y - Service Health Status Should Be Up tedge-mapper-c8y - - ${prefix}= Get Random Name - + # with @id Execute Command tedge mqtt pub --retain 'te/factory/shop/plc1/' '{"@type":"child-device","@id":"${prefix}plc1"}' Execute Command tedge mqtt pub --retain 'te/factory/shop/plc2/' '{"@type":"child-device","@id":"${prefix}plc2"}' Execute Command tedge mqtt pub --retain 'te/factory/shop/plc1/sensor1' '{"@type":"child-device","@id":"${prefix}plc1-sensor1","@parent":"factory/shop/plc1/"}' @@ -246,7 +228,7 @@ Entities persisted and restored Execute Command cat /etc/tedge/.tedge-mapper-c8y/entity_store.jsonl ${original_last_modified_time}= Execute Command date -r /etc/tedge/.tedge-mapper-c8y/entity_store.jsonl - FOR ${counter} IN RANGE 0 5 + FOR ${counter} IN RANGE 0 3 ${timestamp}= Get Unix Timestamp Restart Service tedge-mapper-c8y Service Health Status Should Be Up tedge-mapper-c8y @@ -257,6 +239,9 @@ Entities persisted and restored # Assert that the restored entities are not converted again Should Have MQTT Messages c8y/s/us message_contains=101 date_from=${timestamp} minimum=0 maximum=0 + Should Have MQTT Messages c8y/s/us/${DEVICE_SN}:school:shop:plc1 message_contains=101 date_from=${timestamp} minimum=0 maximum=0 + Should Have MQTT Messages c8y/s/us/${DEVICE_SN}:school:shop:plc1:sensor1 message_contains=102 date_from=${timestamp} minimum=0 maximum=0 + Should Have MQTT Messages c8y/s/us/${DEVICE_SN}:school:shop:plc1:metrics message_contains=102 date_from=${timestamp} minimum=0 maximum=0 Should Have MQTT Messages c8y/s/us/${prefix}plc1 message_contains=101 date_from=${timestamp} minimum=0 maximum=0 Should Have MQTT Messages c8y/s/us/${prefix}plc2 message_contains=101 date_from=${timestamp} minimum=0 maximum=0 Should Have MQTT Messages c8y/s/us/${prefix}plc1 message_contains=102 date_from=${timestamp} minimum=0 maximum=0 @@ -282,6 +267,8 @@ Entities send to cloud on restart External Identity Should Exist ${prefix}plc1-metrics External Identity Should Exist ${prefix}plc2-metrics + Sleep 1s reason=Provide sufficient gap after the last published messages so that the timestamp in the next step is higher than when the first messages published + ${timestamp}= Get Unix Timestamp Restart Service tedge-mapper-c8y Service Health Status Should Be Up tedge-mapper-c8y @@ -295,32 +282,19 @@ Entities send to cloud on restart Should Have MQTT Messages c8y/s/us/${prefix}plc1 message_contains=102,${prefix}plc1-metrics date_from=${timestamp} minimum=1 maximum=1 Should Have MQTT Messages c8y/s/us/${prefix}plc2 message_contains=102,${prefix}plc2-metrics date_from=${timestamp} minimum=1 maximum=1 - *** Keywords *** -Should Have Retained Message Count +Should Have Retained Message Count [Arguments] ${topic} ${exp_count} ${output}= Execute Command mosquitto_sub --retained-only -W 3 -t "${topic}" -v exp_exit_code=27 return_stdout=True Length Should Be ${output.splitlines()} ${exp_count} -Re-enable auto-registration and collect logs - [Teardown] Get Logs ${DEVICE_SN} - Execute Command sudo tedge config unset c8y.entity_store.auto_register - Restart Service tedge-mapper-c8y - Service Health Status Should Be Up tedge-mapper-c8y - -Enable clean start and collect logs - [Teardown] Get Logs ${DEVICE_SN} - Execute Command sudo tedge config set c8y.entity_store.clean_start true - Restart Service tedge-mapper-c8y - Service Health Status Should Be Up tedge-mapper-c8y - Check Child Device [Arguments] ${parent_sn} ${child_sn} ${child_name} ${child_type} ${child_mo}= Device Should Exist ${child_sn} ${child_mo}= Cumulocity.Device Should Have Fragment Values name\=${child_name} - Should Be Equal ${child_mo["owner"]} device_${DEVICE_SN} + Should Be Equal ${child_mo["owner"]} device_${DEVICE_SN} Should Be Equal ${child_mo["name"]} ${child_name} Should Be Equal ${child_mo["type"]} ${child_type} @@ -334,14 +308,12 @@ Check Service Cumulocity.Device Should Exist ${child_sn} show_info=${False} Should Have Services name=${service_name} service_type=${service_type} status=${service_status} +Custom Setup + ${DEVICE_SN}= Setup + Set Test Variable $DEVICE_SN -Test Setup ${CHILD_SN}= Get Random Name Set Test Variable $CHILD_SN Set Test Variable $CHILD_XID ${DEVICE_SN}:device:${CHILD_SN} - ThinEdgeIO.Set Device Context ${DEVICE_SN} - -Custom Setup - ${DEVICE_SN}= Setup - Set Suite Variable $DEVICE_SN + ThinEdgeIO.Set Device Context ${DEVICE_SN} \ No newline at end of file