Create Twin Models

The Twins from Model contain data about the real asset, concept or person, whereas the Digital Twin Model serves as the template or structure of Digital Twins from Model.

In line with our best practice, this page explains how to create Digital Twin Models and, in turn, how to create Digital Twins according to that same Model.


Create Digital Twin Models

Digital Twin Models are used to describe the template or structure of one or more Digital Twin from Model. You may therefore think of a Digital Twin Model as the parent of one or more Digital Twins from Model. Consider that:

  1. Digital Twin Models ensure standardisation: all Digital Twins have the same properties, Feeds and Values as their Model. Some exceptions apply to the Twin's settings, see here;
  2. Digital Twins can be grouped by Model: this simplifies operations like searching, filtering or deleting more than one Twin;
  3. Models only serve as a template for other Digital Twins: they don't share data or follow feeds. These functions are only performed by Twins from Model.
591

Twin Model representation

Create Twin Models with the IOTICSpace User Interface

Navigate to the _Models _section on your IOTICSpace user interface to create a new Digital Twin Model, its Metadata properties and Data feeds.

๐Ÿ“˜

Go to https://{your-space-name}.iotics.space to access the IOTICSpace user interface.

Click for a step-by-step guide and screenshots

2641

Step 1: Click + New Model to create a new Digital Twin Model.

2640

Step 2: Enter Name and Description of your new Digital Twin Model, and click Save.

2635

Step 3: Change Findability to Private to allow no other IOTICSpace to find your Digital Twin Model, or to Public to make it findable by all.

๐Ÿšง

We use Findability and Visibility interchangeably

2631

Step 4: Add metadata properties by clicking the + icon next to Metadata. Search for specific properties and click Add once you're happy with the selection. If you would like to add more than one property group (or "Parent Class"), simply repeat the process as there is no limit on the number of classes per Model. Contact us to add properties which are not yet available.

2630

Step 5: Expand the "Parent Class" to see all the properties that have been added.

2614

Step 6: Click the + button next to Feeds to add data feeds to the Model. For each data feed, add a name and description as well as one or more data fields. Each Model can have none, one or more than one data feed, and each data feed can have one or more data fields.

๐Ÿšง

Don't forget to click Save once you're done editing your Digital Twin Model.

Create Twin Models with the IOTICS API

A Digital Twin Model can be created with the IOTICS API by creating a Digital Twin with Create Twin and specifying their property type = Model. The use of this property allows any Twin to be identified as a Twin Model.

{
    "key": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
    "uriValue": {"value": "https://data.iotics.com/app#Model"}
}

The following non-mandatory properties are important to be added to the Twin Model for it to show in the IOTICS user interface:

{
    "key": "http://www.w3.org/2000/01/rdf-schema#label",
    "langLiteralValue": {"value": "twin_label_model", "lang": "en"},  // e.g.: "Temperature Sensor Model"
},
{
    "key": "http://www.w3.org/2000/01/rdf-schema#comment",
    "langLiteralValue": {"value": "twin_comment", "lang": "en"},  // e.g.: "A temperature sensor that shares temperature data"
},
{
    "key": "https://data.iotics.com/app#color",
    "stringLiteralValue": {"value": "colour_hex_code"},  // e.g.: "#d6df23"
},
{
    "key": "https://data.iotics.com/app#spaceName",
    "stringLiteralValue": {"value": "space_name"},  // e.g. "uk-metoffice"
}

Don't forget to check the Digital Twin's access permissions. You can update the hostAllowList and hostMetadataAllowList properties to enable or restrict access permissions for the Digital Twins according to Selective Sharing for Metadata and Data.

{
    "key": "http://data.iotics.com/public#hostAllowList",
    "uriValue": {"value": "http://data.iotics.com/public#all"},
},
{
    "key": "http://data.iotics.com/public#hostMetadataAllowList",
    "uriValue": {"value": "http://data.iotics.com/public#all"},
}

Click here to see the prerequisites.

1. Create a Twin
2. Add Twin's metadata including the "special" property, Feeds and Values' metadata
from typing import List

from iotics.lib.identity.api.high_level_api import get_rest_high_level_identity_api
from requests import request

RESOLVER_URL = "resolver_url"
HOST = "host_url"

USER_KEY_NAME = "user_key_name"
AGENT_KEY_NAME = "agent_key_name"
USER_SEED = bytes.fromhex("user_seed")
AGENT_SEED = bytes.fromhex("agent_seed")


class IoticsRest:
    def __init__(self):
        self._high_level_api = get_rest_high_level_identity_api(
            resolver_url=RESOLVER_URL
        )

        (
            self._user_registered_id,
            self._agent_registered_id,
        ) = self._high_level_api.create_user_and_agent_with_auth_delegation(
            user_seed=USER_SEED,
            user_key_name=USER_KEY_NAME,
            agent_seed=AGENT_SEED,
            agent_key_name=AGENT_KEY_NAME,
            delegation_name="#AuthDeleg",
        )

        token = self._high_level_api.create_agent_auth_token(
            agent_registered_identity=self._agent_registered_id,
            user_did=self._user_registered_id.did,
            duration=10,
        )

        self._headers = {
            "accept": "application/json",
            "Iotics-ClientAppId": "example_code",
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        }

    def _make_api_call(self, method: str, url: str, json: dict = None):
        response = request(method=method, url=url, headers=self._headers, json=json)
        response.raise_for_status()

        return response.json()

    def create_twin_identity(self, twin_key_name: str):
        twin_registered_id = self._high_level_api.create_twin_with_control_delegation(
            twin_seed=AGENT_SEED,
            twin_key_name=twin_key_name,
            agent_registered_identity=self._agent_registered_id,
            delegation_name="#ControlDeleg",
        )

        return twin_registered_id.did

    def get_host_id(self):
        host_id = self._make_api_call(method="GET", url=f"{HOST}/qapi/host/id")

        return host_id["hostId"]

    def upsert_twin(
        self,
        twin_did: str,
        host_id: str,
        feeds: List[dict] = None,
        inputs: List[dict] = None,
        location: dict = None,
        properties: List[dict] = None,
    ):
        payload = {"twinId": {"hostId": host_id, "id": twin_did}}

        if location:
            payload["location"] = location
        if feeds:
            payload["feeds"] = feeds
        if inputs:
            payload["inputs"] = inputs
        if properties:
            payload["properties"] = properties

        self._make_api_call(method="PUT", url=f"{HOST}/qapi/twins", json=payload)


def main():
    iotics_rest = IoticsRest()
    twin_did = iotics_rest.create_twin_identity(twin_key_name="TwinModel")
    host_id = iotics_rest.get_host_id()
    iotics_rest.upsert_twin(
        twin_did=twin_did,
        host_id=host_id,
        properties=[
            # Model Property
            {
                "key": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
                "uriValue": {"value": "https://data.iotics.com/app#Model"},
            },
            # Label
            {
                "key": "http://www.w3.org/2000/01/rdf-schema#label",
                "langLiteralValue": {"value": "Twin Model", "lang": "en"},
            },
            # Comment
            {
                "key": "http://www.w3.org/2000/01/rdf-schema#comment",
                "langLiteralValue": {
                    "value": "An example of a Twin Model",
                    "lang": "en",
                },
            },
            # Colour
            {
                "key": "https://data.iotics.com/app#color",
                "stringLiteralValue": {"value": "#d6df23"},
            },
            # Space Name
            {
                "key": "https://data.iotics.com/app#spaceName",
                "stringLiteralValue": {"value": "my-space-name"},
            },
        ],
    )


if __name__ == "__main__":
    main()

When creating and updating Digital Twin Models, consider the following best practices:

  1. Could I make my code more efficient? When creating Twin Models with the IOTICS API, it may be easier to use the Upsert Twin API in order to create the Twin, create Feed(s), Values and adding metadata with a single call rather than using multiple APIs separately.
  2. Can I easily identify the Models from Twins from Model? It is good practice to add the word "Model" to the name (label property) of the Twin (e.g. Weather Forecast Model). This way all the Twin Models can easily be distinguished by users of the API and the IOTICS user interface when listing all Twins.
  3. Have I added all properties? Check the list of properties above to make sure your Model shows up correctly in the IOTICS user interface too. Also consider that Twin Models don't need a Location, as it's meaningless.

Create Digital Twins from Model

Create Twins from Model with the IOTICSpace User Interface

Navigate to the _Twins _section on your IOTICSpace User Interface to create a new Digital Twin from an existing Digital Twin Model.

Click for a step-by-step guide and screenshots

2640

Step 1: Click + New Twin to create a new Digital Twin.

2645

Step 2: Enter Name and Description, and select the Digital Twin Model for your new Digital Twin. Click Save.

2631

Step 3: The new Digital Twin has been created. Add values for each or some of the properties, and click the green button to Save.

2638

Step 4: Similarly, for each data feed, add values for each or some data fields, and click the green button to Save. Repeat Step 3 and 4 each time you'd like to add or update a value manually. Use the API to update Digital Twins in bulk.

2639

Step 5: Navigate back to the Twins section to see a list of all your Digital Twins. Alternatively, in the Models section, you can see all Digital Twins for each Model (as seen here).

Create Twins from Model with the IOTICS API

In order to easily identify all the Digital Twins that have been created from a specific Model, add the following property to your new Digital Twins. All Twins that include the below property will be categorised by the IOTICS user interface as "Twins from Model".

{
    "key": "https://data.iotics.com/app#model",
    "uriValue": {"value": model_twin_did},
}

Since the Twin Model serves as a template for the Twins from Model, you also need to make sure that the new Twins follow the same structure as their Model - this means the metadata properties of the Twin, Feed(s) and each of the Feed's Values need to match.

โ—๏ธ

Exceptions: adapt Label, Comment, Type

Although the new Twins need to follow the structure of their Model in terms of metadata, there are some properties that require some modification:

  • Each of the Twin's Label and potentially Comment property values should be unique, and therefore different from both their Model and any other Twin from that Model;
  • The property type: Model that identifies a Twin Model needs to be replaced by the property model: model_twin_did that identifies the Twins from Model;

Click here to see the prerequisites.

1. Search and describe the Twin Model
2. Create a Twin
3. Add Twin's metadata, Feed(s) and Value(s) according to the Model
4. Add the "special" property for Twins from Model
5. Consider the exceptions
import json
from datetime import datetime, timedelta, timezone
from typing import List

from iotics.lib.identity.api.high_level_api import get_rest_high_level_identity_api
from requests import request

RESOLVER_URL = "resolver_url"
HOST = "host_url"

USER_KEY_NAME = "user_key_name"
AGENT_KEY_NAME = "agent_key_name"
USER_SEED = bytes.fromhex("user_seed")
AGENT_SEED = bytes.fromhex("agent_seed")


class IoticsRest:
    def __init__(self):
        self._high_level_api = get_rest_high_level_identity_api(
            resolver_url=RESOLVER_URL
        )

        (
            self._user_registered_id,
            self._agent_registered_id,
        ) = self._high_level_api.create_user_and_agent_with_auth_delegation(
            user_seed=USER_SEED,
            user_key_name=USER_KEY_NAME,
            agent_seed=AGENT_SEED,
            agent_key_name=AGENT_KEY_NAME,
            delegation_name="#AuthDeleg",
        )

        token = self._high_level_api.create_agent_auth_token(
            agent_registered_identity=self._agent_registered_id,
            user_did=self._user_registered_id.did,
            duration=60,
        )

        self._headers = {
            "accept": "application/json",
            "Iotics-ClientAppId": "example_code",
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        }

    def _make_api_call(self, method: str, url: str, json: dict = None):
        response = request(method=method, url=url, headers=self._headers, json=json)
        response.raise_for_status()

        return response.json()

    def create_twin_identity(self, twin_key_name: str):
        twin_registered_id = self._high_level_api.create_twin_with_control_delegation(
            twin_seed=AGENT_SEED,
            twin_key_name=twin_key_name,
            agent_registered_identity=self._agent_registered_id,
            delegation_name="#ControlDeleg",
        )

        return twin_registered_id.did

    def get_host_id(self):
        host_id = self._make_api_call(method="GET", url=f"{HOST}/qapi/host/id")

        return host_id["hostId"]

    def upsert_twin(
        self,
        twin_did: str,
        host_id: str,
        feeds: List[dict] = None,
        inputs: List[dict] = None,
        location: dict = None,
        properties: List[dict] = None,
    ):
        payload = {"twinId": {"hostId": host_id, "id": twin_did}}

        if location:
            payload["location"] = location
        if feeds:
            payload["feeds"] = feeds
        if inputs:
            payload["inputs"] = inputs
        if properties:
            payload["properties"] = properties

        self._make_api_call(method="PUT", url=f"{HOST}/qapi/twins", json=payload)

    def describe_feed(self, twin_did: str, host_id: str, feed_id: str):
        feed_description = self._make_api_call(
            method="GET",
            url=f"{HOST}/qapi/hosts/{host_id}/twins/{twin_did}/feeds/{feed_id}",
        )

        return feed_description

    def describe_input(self, twin_did: str, host_id: str, input_id: str):
        input_description = self._make_api_call(
            method="GET",
            url=f"{HOST}/qapi/hosts/{host_id}/twins/{twin_did}/inputs/{input_id}",
        )

        return input_description

    def search_twins(
        self,
        text: str = None,
        location: dict = None,
        properties: List[dict] = None,
        scope: str = "LOCAL",
        response_type: str = "FULL",
    ):
        twins_list = []

        search_headers = self._headers.copy()
        # Search headers require a new header "Iotics-RequestTimeout".
        # The latter is used to stop the request once the timeout is reached
        search_headers.update(
            {
                "Iotics-RequestTimeout": (
                    datetime.now(tz=timezone.utc) + timedelta(seconds=5)
                ).isoformat(),
            }
        )

        payload = {"responseType": response_type, "filter": {}}

        if text:
            payload["filter"]["text"] = text
        if properties:
            payload["filter"]["properties"] = properties
        if location:
            payload["filter"]["location"] = location

        with request(
            method="POST",
            url=f"{HOST}/qapi/searches",
            headers=search_headers,
            stream=True,
            verify=True,
            params={"scope": scope},
            json=payload,
        ) as resp:
            resp.raise_for_status()
            # Iterates over the response data, one Host at a time
            for chunk in resp.iter_lines():
                response = json.loads(chunk)
                twins_found = []
                try:
                    twins_found = response["result"]["payload"]["twins"]
                except KeyError:
                    continue
                finally:
                    if twins_found:
                        # Append the twins found to the list of twins
                        twins_list.extend(twins_found)

        print(f"Found {len(twins_list)} twin(s)")

        return twins_list

    def create_twin_from_model(self, twin_model: dict, label: str, comment: str):
        twin_model_did = twin_model["twinId"]["id"]
        twin_model_host_id = twin_model["twinId"]["hostId"]

        # Get the Twin Model's list of properties
        twin_model_properties = twin_model["properties"]

        # Build a list of properties for the Twin from Model
        twin_from_model_property_list = [
            # Add the Twins' from Model "special" property
            {
                "key": "https://data.iotics.com/app#model",
                "uriValue": {"value": twin_model_did},
            }
        ]

        # Add all the Twin Model's properties but consider the exceptions
        for property in twin_model_properties:
            # This was the "special" property of the Twin Model, we don't need it
            if property["key"] == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type":
                continue
            # Change the Label value
            if property["key"] == "http://www.w3.org/2000/01/rdf-schema#label":
                property["langLiteralValue"]["value"] = label
            # Change the Comment value
            if property["key"] == "http://www.w3.org/2000/01/rdf-schema#comment":
                property["langLiteralValue"]["value"] = comment

            twin_from_model_property_list.append(property)

        # Build a list of Feeds for the Twin from Model
        feeds_metadata_list = []
        # Scan the Twin Model's Feeds and use the Feed Describe API to get the Feeds' metadata
        for feed in twin_model["feeds"]:
            feed_id = feed["feedId"]["id"]

            # Use the Feed Describe API
            feed_description = self.describe_feed(
                twin_did=twin_model_did, host_id=twin_model_host_id, feed_id=feed_id
            )
            feed_properties = feed_description["result"]["properties"]
            feed_values = feed_description["result"]["values"]
            store_last = feed_description["result"]["storeLast"]

            # Append to the list all the Feed's metadata
            feeds_metadata_list.append(
                {
                    "id": feed_id,
                    "properties": feed_properties,
                    "values": feed_values,
                    "storeLast": store_last,
                }
            )

        # Build a list of Inputs for the Twin from Model
        inputs_metadata_list = []
        # Scan the Twin Model's Inputs and use the Input Describe API to get the Inputs' metadata
        for input in twin_model["inputs"]:
            input_id = input["inputId"]["id"]

            # Use the Input Describe API
            input_description = self.describe_input(
                twin_did=twin_model_did, host_id=twin_model_host_id, input_id=input_id
            )
            input_properties = input_description["result"]["properties"]
            input_values = input_description["result"]["values"]

            # Append to the list all the Inputs's metadata
            inputs_metadata_list.append(
                {"id": input_id, "properties": input_properties, "values": input_values}
            )

        return twin_from_model_property_list, feeds_metadata_list, inputs_metadata_list


def main():
    iotics_rest = IoticsRest()
    twin_did = iotics_rest.create_twin_identity(twin_key_name="TwinFromModel")
    host_id = iotics_rest.get_host_id()
    twins_list = iotics_rest.search_twins(
        properties=[
            {
                "key": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
                "uriValue": {"value": "https://data.iotics.com/app#Model"},
            }
        ]
    )

    # In this example it is assumed the Twin Model to use is the first one in the list
    twin_model = next(iter(twins_list))
    (
        twin_from_model_property_list,
        feeds_metadata_list,
        inputs_metadata_list,
    ) = iotics_rest.create_twin_from_model(
        twin_model=twin_model,
        label="Temperature Sensor",
        comment="An example of a Twin from Model",
    )
    iotics_rest.upsert_twin(
        twin_did=twin_did,
        host_id=host_id,
        feeds=feeds_metadata_list,
        inputs=inputs_metadata_list,
        properties=twin_from_model_property_list,
    )


if __name__ == "__main__":
    main()