Azure IoT DeviceClient SDK Python demonstration, the basics

In the past, I wrote a blog about the Azure IoT device SDK. This example was written in C#.

Last year, I noticed an increased number of questions coming from Python users trying to connect devices to the cloud. Luckily, the driving force behind this is the growing community of ML developers using Python. They are increasingly involved in IoT projects.

That’s why I scrambled some samples together into one demonstration showing the capabilities of Azure IoT Hub-connected devices.

We will see how device-to-cloud messages are sent from the device to your IoT Hub. And we will see several ways of cloud-to-device communication so we can enforce actions on the device.

Update: recently, I added a second Python script with an individual Device Provisioning Service Enrollment based on a symmetric key. The example is exactly the same, you only need to provide other variables for this provisioning.

This introduction will get you started within a moment.

The Azure IoT Hub is a so-called cloud gateway for IoT Devices. It serves four main purposes:

  1. Device identities can be registered so each device has its own credentials
  2. Devices can send data to the IoT Hub in a secure and reliable way
  3. Incoming messages are stored and forwarded (routed) to other Azure resources
  4. If needed, cloud-to-device communication can be offered to execute actions on the device

We will explore these features below.

If you want to follow along, you need several things:

So, get a (free) Azure Subscription, install Python and Visual Studio code so you can start programming, install the IoT Explorer to play with the cloud part of our demo solution, and run the sample code.

Registering devices

IoT Devices need to be registered in the IoT Hub first. Only then, for each device, specific security credentials are provided which can be used by only one device.

Note: In this example, we use a symmetric key. This is OK for testing purposes only.

In the Azure portal, create an IoT Hub and register a device. You could follow this Python example.

Here, we generate a symmetric key for a new device. I named my device ‘BasicDevice’:

Note: there are two keys. This makes it possible to deprecate keys occasionally forcing devices to use the latest currently valid key. Your devices need extra care when rolling the keys.

Here, the primary connection string must be saved in the environment variables so our code can access the key:

Note: this is a good coding practice, preventing having your device credentials checked in into your version control by accident. Don’t mix them with code. There are several other solutions (like the .env file in Docker though.)

The code we use is available on GitHub.

Sending messages to the cloud

The basic sample I started with already supported uploading random messages, including application properties:

# *** Sending a message ***
#
# Build the message with simulated telemetry values.
temperature = TEMPERATURE + (random.random() * 15)
humidity = HUMIDITY + (random.random() * 20)
msg_txt_formatted = MSG_TXT.format(temperature=temperature, humidity=humidity)
message = Message(msg_txt_formatted)

message.content_encoding = "utf-8"
message.content_type = "application/json"

# Add a custom application property to the message.
# An IoT hub can filter on these properties without access to the message body.
if temperature > 30:
    message.custom_properties["temperatureAlert"] = "true"
else:
    message.custom_properties["temperatureAlert"] = "false"

# Send the message.
print("Sending message: {}".format(message))
client.send_message(message)
print("Message sent")

A Message object is first constructed and then sent using the IoT Hub device client object.

Notice I added the content encoding and content type properties. These can be omitted if you are going to use IoT Hub routing without the message body values. In real life, it’s better to add them already because then you do not want to update your code on all devices just because you changed your mind.

Running this code only works if you load the correct Python packages. This can be done with PIP on the Dos prompt:

pip install azure.iot.device

Once the packages are in place and the connection string is put into the right environment variable, you can run the python code using Visual Studio Code.

Just hit the F5 button and see how the code runs:

Note: you can even add breakpoints to debug the code or watch variable values.

Using the Azure IoT Explorer, you can see the incoming messages arrive at the IoT Hub.

First, add an IoT Hub connection to the explorer using the ‘iothubowner’ shared access policy.

Warning: this key is quite important. It unlocks full access to the IoT Hub. In production, create a separate policy exposing just the policies you need.

Take that IoT Hub connection string to make a connection:

From there, navigate to our ‘BasicDevice’ device registration and select ‘Telemetry’:

We see the messages arrive at the IoT Hub.

Note: The Python SDK does not support sending a batch of messages, as seen in the C# SDK.

Notice, for ‘consumer group’, I use ‘$default’. This is ok for now, but it could cause problems soon.

Azure resources, consuming telemetry messages from the IoT Hub (like Azure Function, Stream Analytics, or Time Series Insights), get a copy of each incoming message based on that ‘consumer group’. In this case, if another resource uses that same ‘$default’, this can cause errors.

I recommend giving each ‘consumer’ a separate consumer group. In this case, I added this ‘explorer’ consumer group:

I use that in the IoT explorer:

Define your own naming convention for other consumer groups references by other Azure resources.

You are now able to check incoming Device-to-Cloud messages in a reliable way.

Cloud-to-Device communication

The Azure IoT Hub supports two-way communication.

We are able to manipulate the device using several techniques:

  1. Direct Methods
  2. Cloud Messages
  3. Desired properties (and reported properties)

Direct Methods provides a live update of a connected device. You can send a (JSON) message to the device while it is connected to the IoT Hub, and you can wait for a response back. Notice that a time-out can occur both when the device connection fails or when the response is not generated within some time constraint.

Cloud messages can be seen as simplified direct messages. Yes, you can send a message, but you do not have to wait for it to be picked up. It’s like fire-and-forget. The message is stored in the IoT Hub until the device picks it up (when the device connects to the IoT Hub). There is no response message involved. By the way, from the cloud side, you can check if messages are picked up already…

Another way to communicate is using the Device Twin:

Each registered device in the IoT Hub has its own Device Twin. This is just a document holding the current/last known state of the device.

Users can put values in the Desired properties even when the device is offline.

When the device connects, it gets the desired properties and can respond to it. To notify the user of the changed state, the device can write back reported properties. Again, only when the device connects.

Let’s see how these separate means of communication can be implemented.

Cloud-to-Device: Direct Methods

To implement a Direct Method, I started by adding extra code to our code example.

First, we need a request handler on the device client which reacts to all incoming Direct Method calls:

# Attach the direct method request handler
client.on_method_request_received = method_request_handler

Then, we need the actual code to handle the request.

Here, we react only on methods with the name “SetTelemetryInterval”. Here, we expect just an integer inside the payload of the request:

# *** Direct Method ***
#
# Define a method request handler
def method_request_handler(method_request):
    
    print(MSG_LOG.format(name=method_request.name, payload=method_request.payload))

    if method_request.name == "SetTelemetryInterval":
        try:
            global INTERVAL
            INTERVAL = int(method_request.payload)
        except ValueError:
            response_payload = {"Response": "Invalid parameter"}
            response_status = 400
        else:
            response_payload = {"Response": "Executed direct method {}, interval updated".format(method_request.name)}
            response_status = 200
    else:
        response_payload = {"Response": "Direct method {} not defined".format(method_request.name)}
        response_status = 404

    method_response = MethodResponse.create_from_method_request(method_request, response_status, response_payload)
    client.send_method_response(method_response)

Using the Azure IoT Explorer, we can test the Direct Method:

Note: we can modify the timeouts if needed on the cloud side.

If I invoke this method, while the device client is connected, I see this response in the device log:

The method is executed, and the response is as expected (200):

When I try a call using an unknown method name, it is rejected (404):

If the device is not connected while I make send a method, I see this server-side timeout:

Direct methods are especially useful when you want to have full control over the device. Still, the device must be online at that exact moment.

Cloud-to-Device: Cloud messages

If a device is not always accessible, a cloud message could be the solution.

Just send some body text as cloud message (if needed, you can include custom properties too) and you’re done:

On the device, you must listen to these cloud messages:

# Attach the cloud message request handler
client.on_message_received = message_received_handler

The handler could look like this:

    # *** Cloud message ***
    #
    # define behavior for receiving a message
    def message_received_handler(message):
        print("the data in the message received was ")
        print(message.data)
        print("custom properties are")
        print(message.custom_properties)

When the message is invoked while the device is running, the message is handled as expected:

When I send another message while the device is still offline, it is first stored in the IoT Hub:

This other message is then received as soon as the device starts:

Perhaps, the device is not capable of connecting to the cloud but normally the logic is still running. So, the message will be received as soon as it connects to the cloud.

Note: The Python Device SDK is not capable to reject or abandon received messages (as seen in the C# SDK).

Cloud-to-Device: Desired property changes

The third way to send cloud-to-device messages is using the Device twin.

When the device starts and connects, we can check the current twin as seen in the cloud:

client.connect()
twin = client.get_twin()
print ( "Twin at startup is" )
print ( twin )

In the IoT Explorer, I already filled in this desired property:

When I start the application, we see the arrival of both desired properties and reported properties:

Next to that, we are listening to desired property changes:

# Attach the Device Twin Desired properties change request handler
client.on_twin_desired_properties_patch_received = twin_patch_handler

When any desired property in the cloud is added, changed, or deleted, we are notified:

# *** Device Twin ***
#
# define behavior for receiving a twin patch
# NOTE: this could be a function or a coroutine
def twin_patch_handler(patch):
    print("the data in the desired properties patch was: {}".format(patch))
    # Update reported properties with cellular information
    print ( "Sending data as reported property..." )
    reported_patch = {"reportedValue": 42}
    client.patch_twin_reported_properties(reported_patch)
    print ( "Reported properties updated" )

In the IoT Explorer, I then add a second property while the device application is running:

So, we see the arrival of that change:

If you checked out the code already, you will have noticed the device reports back a reported property name “reportedValue”.

This reported value (42) is now available in the cloud (as seen in the IoT Explorer after a refresh):

If you want to remove a reported property, this is also supported in the device SDK. Just send the value ‘ None’ (which represents null in JSON):

Normally, devices are capable of saving some state on their own. Use this capability so a device can survive a reboot without losing its current desired property values and thus starting up in the original ‘hard-coded’ state, every time the internet connection is not yet available.

Azure IoT Plug and Play support

Although the Azure IoT Plug and Play functionality is out of scope in this post (check out this post to see what it means and how it works), it is supported by the Python SDK.

We need to supply a so-called PnP modelId while connecting. This is done like so:

# Create an IoT Hub client

model_id = "dtmi:com:example:NonExistingController;1"

client = IoTHubDeviceClient.create_from_connection_string(
            CONNECTION_STRING,
            product_info=model_id)

Once this code is executed while the device connects to the IoT Hub, the modelId is registered in the DeviceTwin:

Check the usage in this post. A more elaborate coding Python Azure IoT PnP example is seen here.

Conclusion

This post will help you when you try to build your first Azure IoT device written in Python. It uses the Azure IoT Device SDK, so you get a head start.

Experiment with it and try to ruggedize it when needed in your environment.

If you are interested in more advanced samples (with excellent features like device provisioning, x509 support, proxy usage, or file upload), check out this GitHub repository.

Here, we see the demonstration of an IoT device, connected to the IoT Hub.

Azure IoT also supports Edge solutions based on docker containers. If you are interested in these kinds of IoT Edge solutions, check out the Python Azure IoT Edge module example.