Ingest IoT Hub telemetry into Azure Digital Twins

This guide walks through the process of writing a function that can ingest device telemetry from IoT Hub and send it to an instance of Azure Digital Twins.

Azure Digital Twins is driven with data from IoT devices and other sources. A common source for device data to use in Azure Digital Twins is IoT Hub.

The process for ingesting data into Azure Digital Twins is to set up an external compute resource, such as a function that's made by using Azure Functions. The function receives the data and uses the DigitalTwins APIs to set properties or fire telemetry events on digital twins accordingly.

This how-to document walks through the process for writing a function that can ingest device telemetry from IoT Hub.

Prerequisites

Before continuing with this example, you'll need to set up the following resources as prerequisites:

Example telemetry scenario

This how-to outlines how to send messages from IoT Hub to Azure Digital Twins, using a function in Azure. There are many possible configurations and matching strategies you can use for sending messages, but the example for this article contains the following parts:

  • A thermostat device in IoT Hub, with a known device ID
  • A digital twin to represent the device, with a matching ID

Note

This example uses a straightforward ID match between the device ID and a corresponding digital twin's ID, but it is possible to provide more sophisticated mappings from the device to its twin (such as with a mapping table).

Whenever a temperature telemetry event is sent by the thermostat device, a function processes the telemetry and the Temperature property of the digital twin should update. This scenario is outlined in a diagram below:

Diagram of IoT Hub device sending Temperature telemetry to a function in Azure, which updates a Temperature property on a twin in Azure Digital Twins.

Add a model and twin

In this section, you'll set up a digital twin in Azure Digital Twins that will represent the thermostat device and will be updated with information from IoT Hub.

To create a thermostat-type twin, you'll first need to upload the thermostat model to your instance, which describes the properties of a thermostat and will be used later to create the twin.

The model looks like this:

{
    "@id": "dtmi:contosocom:DigitalTwins:Thermostat;1",
    "@type": "Interface",
    "@context": "dtmi:dtdl:context;3",
    "contents": [
      {
        "@type": "Property",
        "name": "Temperature",
        "schema": "double"
      }
    ]
  }

To upload this model to your twins instance, run the following Azure CLI command, which uploads the above model as inline JSON. You can run the command in Azure Cloud Shell in your browser (use the Bash environment), or on your machine if you have the CLI installed locally. There's one placeholder for the instance's host name (you can also use the instance's friendly name with a slight decrease in performance).

az dt model create --dt-name <instance-hostname-or-name> --models '{  "@id": "dtmi:contosocom:DigitalTwins:Thermostat;1",  "@type": "Interface",  "@context": "dtmi:dtdl:context;2",  "contents": [    {      "@type": "Property",      "name": "Temperature",      "schema": "double"    }  ]}' 

Note

If you're using anything other than Cloud Shell in the Bash environment, you may need to escape certain characters in the inline JSON so that it's parsed correctly. For more information, see Use special characters in different shells.

You'll then need to create one twin using this model. Use the following command to create a thermostat twin named thermostat67, and set 0.0 as an initial temperature value. There's one placeholder for the instance's host name (you can also use the instance's friendly name with a slight decrease in performance).

az dt twin create  --dt-name <instance-hostname-or-name> --dtmi "dtmi:contosocom:DigitalTwins:Thermostat;1" --twin-id thermostat67 --properties '{"Temperature": 0.0}'

When the twin is created successfully, the CLI output from the command should look something like this:

{
  "$dtId": "thermostat67",
  "$etag": "W/\"0000000-9735-4f41-98d5-90d68e673e15\"",
  "$metadata": {
    "$model": "dtmi:contosocom:DigitalTwins:Thermostat;1",
    "Temperature": {
      "lastUpdateTime": "2021-09-09T20:32:46.6692326Z"
    }
  },
  "Temperature": 0.0
}

Create the Azure function

In this section, you'll create an Azure function to access Azure Digital Twins and update twins based on IoT device telemetry events that it receives. Follow the steps below to create and publish the function.

  1. First, create a new Azure Functions project of Event Grid trigger type.

    You can do this using Visual Studio (for instructions, see Develop Azure Functions using Visual Studio), Visual Studio Code (for instructions, see Create a C# function in Azure using Visual Studio Code), or the Azure CLI (for instructions, see Create a C# function in Azure from the command line).

  2. Add the following packages to your project (you can use the Visual Studio NuGet package manager, or the dotnet add package command in a command-line tool).

  3. Create a function within the project called IoTHubtoTwins.cs. Paste the following code into the function file:

    using System;
    using Azure;
    using System.Net.Http;
    using Azure.Core.Pipeline;
    using Azure.DigitalTwins.Core;
    using Azure.Identity;
    using Microsoft.Azure.WebJobs;
    using Microsoft.Azure.WebJobs.Extensions.EventGrid;
    using Microsoft.Extensions.Logging;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    using Azure.Messaging.EventGrid;
    
    namespace IotHubtoTwins
    {
        public class IoTHubtoTwins
        {
            private static readonly string adtInstanceUrl = Environment.GetEnvironmentVariable("ADT_SERVICE_URL");
            private static readonly HttpClient httpClient = new HttpClient();
    
            [FunctionName("IoTHubtoTwins")]
            // While async void should generally be used with caution, it's not uncommon for Azure function apps, since the function app isn't awaiting the task.
    #pragma warning disable AZF0001 // Suppress async void error
            public async void Run([EventGridTrigger] EventGridEvent eventGridEvent, ILogger log)
    #pragma warning restore AZF0001 // Suppress async void error
            {
                if (adtInstanceUrl == null) log.LogError("Application setting \"ADT_SERVICE_URL\" not set");
    
                try
                {
                    // Authenticate with Digital Twins
                    var cred = new DefaultAzureCredential();
                    var client = new DigitalTwinsClient(new Uri(adtInstanceUrl), cred);
                    log.LogInformation($"ADT service client connection created.");
                
                    if (eventGridEvent != null && eventGridEvent.Data != null)
                    {
                        log.LogInformation(eventGridEvent.Data.ToString());
    
                        // <Find_device_ID_and_temperature>
                        JObject deviceMessage = (JObject)JsonConvert.DeserializeObject(eventGridEvent.Data.ToString());
                        string deviceId = (string)deviceMessage["systemProperties"]["iothub-connection-device-id"];
                        var temperature = deviceMessage["body"]["Temperature"];
                        // </Find_device_ID_and_temperature>
    
                        log.LogInformation($"Device:{deviceId} Temperature is:{temperature}");
    
                        // <Update_twin_with_device_temperature>
                        var updateTwinData = new JsonPatchDocument();
                        updateTwinData.AppendReplace("/Temperature", temperature.Value<double>());
                        await client.UpdateDigitalTwinAsync(deviceId, updateTwinData);
                        // </Update_twin_with_device_temperature>
                    }
                }
                catch (Exception ex)
                {
                    log.LogError($"Error in ingest function: {ex.Message}");
                }
            }
        }
    }
    

    Save your function code.

  4. Publish the project with the IoTHubtoTwins.cs function to a function app in Azure.

    For instructions on how to publish the function using Visual Studio, see Develop Azure Functions using Visual Studio. For instructions on how to publish the function using Visual Studio Code, see Create a C# function in Azure using Visual Studio Code. For instructions on how to publish the function using the Azure CLI, see Create a C# function in Azure from the command line.

Once the process of publishing the function completes, you can use this Azure CLI command to verify the publish was successful. There are placeholders for your resource group, and the name of your function app. The command will print information about the IoTHubToTwins function.

az functionapp function show --resource-group <your-resource-group> --name <your-function-app> --function-name IoTHubToTwins

Configure the function app

To access Azure Digital Twins, your function app needs a system-assigned managed identity with permissions to access your Azure Digital Twins instance. You'll set that up in this section, by assigning an access role for the function and configuring the application settings so that it can access your Azure Digital Twins instance.

Run the following commands in Azure Cloud Shell or a local Azure CLI.

Note

This section must be completed by an Azure user who has permissions to manage user access to Azure resources, including granting and delegating permissions. Common roles that meet this requirement are Owner, Account admin, or the combination of User Access Administrator and Contributor. For more information about permission requirements for Azure Digital Twins roles, see Set up an instance and authentication.

Assign an access role

The Azure function requires a bearer token to be passed to it. To make sure the bearer token is passed, grant the function app the Azure Digital Twins Data Owner role for your Azure Digital Twins instance, which will give the function app permission to perform data plane activities on the instance.

  1. Use the following command to create a system-managed identity for your function (if the function already has one, this command will print its details). Take note of the principalId field in the output. You'll use this ID to refer to the function so that you can grant it permissions in the next step.

    az functionapp identity assign --resource-group <your-resource-group> --name <your-function-app-name>	
    
  2. Use the principalId value in the following command to give the function the Azure Digital Twins Data Owner role for your Azure Digital Twins instance.

    az dt role-assignment create --dt-name <your-Azure-Digital-Twins-instance> --assignee "<principal-ID>" --role "Azure Digital Twins Data Owner"
    

Configure application settings

Next, make the URL of your Azure Digital Twins instance accessible to your function by setting an environment variable for it.

Tip

The Azure Digital Twins instance's URL is made by adding https:// to the beginning of your instance's host name. To see the host name, along with all the properties of your instance, run az dt show --dt-name <your-Azure-Digital-Twins-instance>.

The following command sets an environment variable for your instance's URL that your function will use whenever it needs to access the instance.

az functionapp config appsettings set --resource-group <your-resource-group> --name <your-function-app-name> --settings "ADT_SERVICE_URL=https://<your-Azure-Digital-Twins-instance-host-name>"

Connect the function to IoT Hub

In this section, you'll set up your function as an event destination for the IoT hub device data. Setting up your function in this way will ensure that the data from the thermostat device in IoT Hub will be sent to the Azure function for processing.

Use the following CLI command to create an event subscription that the IoT Hub will use to send event data to the IoTHubtoTwins function. There's a placeholder for you to enter a name for the event subscription, and there are also placeholders for you to enter your subscription ID, resource group, IoT hub name, and the name of your function app.

az eventgrid event-subscription create --name <name-for-hub-event-subscription> --event-delivery-schema eventgridschema --source-resource-id /subscriptions/<your-subscription-ID>/resourceGroups/<your-resource-group>/providers/Microsoft.Devices/IotHubs/<your-IoT-hub> --included-event-types Microsoft.Devices.DeviceTelemetry --endpoint-type azurefunction --endpoint /subscriptions/<your-subscription-ID>/resourceGroups/<your-resource-group>/providers/Microsoft.Web/sites/<your-function-app>/functions/IoTHubtoTwins

The output will show information about the event subscription that has been created. You can confirm that the operation completed successfully by verifying the provisioningState value in the result:

"provisioningState": "Succeeded",

Test with simulated IoT data

You can test your new ingress function by using the device simulator from Connect an end-to-end solution. The DeviceSimulator project contains a simulated thermostat device that sends sample temperature data. To set up the device simulator, follow these steps:

  1. Navigate to the Azure Digital Twins end-to-end sample project repository. Get the sample project on your machine by selecting the Browse code button underneath the title. This will take you to the GitHub repo for the samples, which you can download as a .zip by selecting the Code button followed by Download ZIP.

    This will download a .zip folder to your machine as digital-twins-samples-main.zip. Unzip the folder and extract the files. You'll be using the DeviceSimulator project folder.

  2. Register the simulated device with IoT Hub

  3. Configure and run the simulation

After completing these steps, you should have a project console window running and sending simulated device telemetry data to your IoT hub.

Screenshot of the output from the device simulator project.

Validate results

While running the device simulator above, the temperature value of your thermostat digital twin will be changing. In the Azure CLI, run the following command to see the temperature value. There's one placeholder for the instance's host name (you can also use the instance's friendly name with a slight decrease in performance).

az dt twin query --query-command "SELECT * FROM digitaltwins WHERE \$dtId = 'thermostat67'" --dt-name <instance-hostname-or-name>

Note

If you're using anything other than Cloud Shell in the Bash environment, you may need to escape the $ character in the query differently so that it's parsed correctly. For more information, see Use special characters in different shells.

Your output should show the details of the thermostat67 twin, including a temperature value, like this:

{
  "result": [
    {
      "$dtId": "thermostat67",
      "$etag": "W/\"dbf2fea8-d3f7-42d0-8037-83730dc2afc5\"",
      "$metadata": {
        "$model": "dtmi:contosocom:DigitalTwins:Thermostat;1",
        "Temperature": {
          "lastUpdateTime": "2021-06-03T17:05:52.0062638Z"
        }
      },
      "Temperature": 70.20518558807913
    }
  ]
}

To see the Temperature value change, repeatedly run the query command above.

Next steps

Read about data ingress and egress with Azure Digital Twins: