# Update Module's Object Model

This tutorial will present a basic scenario where module will update its state in the cloud. Every "thing" in ABB Ability™ has its cloud representation in the form of information-model object - this includes our modules. Like any other information-model entity, our module can have: some variables, some methods, some properties, and so on. Anytime the information-model is updated, its version is incremented. Basically, such operation updates the current state that our device/module/thing is represented by in the cloud. Don't treat it interchangeably with what variables are used for - updating properties does not preserve the previous values of them. Another thing to remember is that the described way of updating information-models applies to Ability Edge modules themselves only. Updating modules' devices/children is done in a bit different way.

# Before you start

# Overview

Our module is going to have a method called "ping" - the purpose of this method is to check if the module is online - reachable module would return "200" HTTP status. If you look in properties section of the type definition, you will find a property called pingCount - the value of it should represent the amount of "ping" method invocations. Anytime someone "pings" our module, its pingCount in the information-model object should get updated.

# Method definition

"methods": {
    "ping": {
        "description": "Checks if the module is online",
        "input": {}
    }
}

As you can see, this simple method does not require any inputs.

# Property definition

"pingCount": {
    "dataType": "number",
    "value": 0
}

The property is of type number and its value is initially equal to 0 for any new object.

# Update Module's Device Object Model

# Topic

Before looking at our specific case of updating pingCount, let's look more generally at how Ability Edge enables such procedure. First thing to note is the topic that should be used - it can be read from topics_model_out environment variable. If you intend to update different model that 'abb.ability.device' you should append it at the end of topic name (prefixed with '/').

WARNING

As ABB Ability™ Edge documentation informs, entities themselves cannot update their 'abb.ability.configuration' models - that should to be done from the cloud level only.

In case of our tutorial, we'll be updating 'abb.ability.device' object model.

# Payload

Payload of the information-model update is exactly the same as the one we would use with Instance API's PUT endpoint of objects. It needs to contain information-model itself:

  • objectId,
  • model,
  • type,
  • properties (unique + the ones to be updated, if any).

Additional required parameter is the version from which we are updating. For example, if your current object has version 1, you'd append

(...)
"version" : 1
(...)

tag to your payload. As a result, the version would be bumped to 2. Here's an example of full payload:

{
  "model": "abb.ability.device",
  "type": "abb.ability.device.edge.modules.csharp.update.object.model.sample",
  "properties": {
    "name": {
      "value": "1232121421"
    },
    "pingCount": {
      "value": 7
    }
  },
  "version": 1
}

The example payload above would update the pingCount with value 7. It would be the first update since version tag is at value 1.

# Handling "ping" Invocations

Anytime someone invokes "ping" command/method, our module should send the update request with its pingCount property value incremented. Let's look in details what steps are involved in this process.

# Receiving current state

Before we are able to update our information-model, we need to know our current state. This is possible thanks to existence of topics_model_in topic. This is the channel that delivers information-models to our module (of both the module itself and its children/devices).

TIP

To learn more about topics_model_in topic, read this article: Creating Configurable Edge Modules

Every time your module starts (whether it is due to Edge booting up or due to your module restarting for any reason), it should receive its current information-model. Thanks to it, you are able to:

  • read the current version of it,
  • read the module's name property,
  • read the current value of pingCount.

All these information are necessary for our scenario to work. We cannot update information-model without knowing its current version. Additionally, we need to know the module's name since it is a unique property and each update requires its existence in the payload. The value of this property is partly randomly generated when information-model object is instantiated - that's why the module cannot know its value without information from the cloud.

More than that, we need to know current pingCount not to lose the previous invocations of "ping" command (which could get executed during previous runtime of our module, before Edge restart for example). This is why the following code is needed:

private Task HandleIncomingInfoModelAsync(JObject infoModel)
{
    _moduleDetails = ReadModuleDetails(infoModel);
    var pingCountFromCloud = ReadPingCount(infoModel);
    Console.WriteLine($"Received latest information-model version ({_moduleDetails.Version}) with pingCount = {pingCountFromCloud}");

    _pingCountHandler.InternalPingCount = pingCountFromCloud + _unreportedPingCount;

    if (_unreportedPingCount != 0)
    {
        Console.WriteLine($"There are {_unreportedPingCount} unreported pings. Info-model will be updated.");
        _unreportedPingCount = 0;
        return UpdateModelAsync();
    }

    return Task.CompletedTask;
}

private ModuleDetails ReadModuleDetails(JObject infoModel)
{
    return new ModuleDetails(
        infoModel["type"].Value<string>(),
        infoModel["properties"]["name"]["value"].Value<string>(),
        infoModel["version"].Value<int>());
}

ModuleDetails is a simple POCO class (constructor was omitted, since it just assigns each value):

public class ModuleDetails
{
    public ModuleDetails(string type, string name, int version) { ... }

    public string Type { get; set; }
    public string Name { get; set; }
    public int Version { get; set; }
}

In the code, you can notice two things:

  • usage of _pingCountHandler object,
  • existence of _unreportedPingCount field.

# PingCountHandler

The _pingCountHandler object is an instance of PingCountHandler class - its purpose is to handle ping count operations:

  • storing the current value of pingCount internally
  • updating pingCount in the cloud.

# Caching pingCount

PingCountHandler stores internal value of pingCount for the following reason: imagine that you are about to invoke "ping" command 5 times - in order for the module to be able to update pingCount in the cloud 5 times, it needs to know the previous value of it, each time it updates it (to know the starting value before it increments it). We could fetch this value from the cloud each time, however, it is not the best practice. It's much better, performance-wise, to cache the latest pingCount in the module.

# Updating model

The other responsibility of PingCountHandler is updating the information-model. Here's the source code of this functionality:

public Task UpdateCloudPingCountAsync(ModuleDetails moduleDetails)
{
    if (InternalPingCount.HasValue)
    {
        Console.WriteLine("Updating the module's information-model");

        var payload = new ModuleModelUpdatePayload(
            _objectId,
            moduleDetails.Version,
            moduleDetails.Type,
            new Dictionary<string, PropertyContent> {
                {"pingCount", new PropertyContent(InternalPingCount) },
                {"name", new PropertyContent(moduleDetails.Name)}
        });
        var payloadString = JsonConvert.SerializeObject(payload, GetSerializationSettings());

        return _mqttClient.PublishAsync(_modelUpdateTopic, payloadString, MqttQualityOfServiceLevel.AtLeastOnce)
            .ContinueWith((e) => Console.WriteLine($"Published message to topic '{_modelUpdateTopic}': {payloadString}"));
    }
    else
    {
        throw new InvalidOperationException("UpdateCloudPingCount was invoked when _pingCount was not initialized.");
    }
}

As you can see, updating the information model comes down to publishing the latest version of it on the right topic.

# Unreported pings

It is important to cover every possible situation in your code. One of possibilities that could happen in our case is the invocation of "ping" method before our module learns its current information-model details (version, current pingCount, name). In such situation, module is unable to update the information-model, simply because it lacks necessary information. That's what _unreportedPingCount is for. Value of it is incremented each time "ping" is invoked and module is not yet aware of its details. When these details finally arrive, pingCount is updated automatically according to the number of unreportedPingCount. The described situation would be probably very rare to happen (invocation of method before information-model is delivered to the module), however it needs to be handled properly, even if it's not much likely to occur.

# Running the Sample Module

There are two ways to run this custom module. CST provides types for a given version of Ability Platform. If your CST contact has not already loaded these types into your Ability environment, these types can be manually uploaded to the environment. Once the types are loaded into the instance, any supported Edge setup can be used to run the Edge types provided by the tutorials.

Once you are familiar with the Edge, it is possible to build custom versions of the tutorials. Once a custom modules is created and the type(s) are loaded into an instance, any valid Edge can run the custom type.

# Conclusion

This article dealt with updating module's 'abb.ability.device' information model. As was already stated in the introduction, make sure that you use that functionality in the right way and understand the difference between 'property' and 'variable' concepts of information modeling.

Author: Marcin Jahn
Last updated: 8/5/2021, 5:55:48 AM
Feedback