Getting started with the Cumulocity Python API

My name is Christoph Souris. I’m a consultant at Software AG and one of the creators of the Cumulocity Python API.

In this article, I’d like to give you a step-by-step introduction to the possibilities of this API. We will create a simple application, read device metadata, and create and investigate alarms.

Preliminaries

If you want to follow this guide you’d need access to a Cumulocity IoT tenant (duh!). If you don’t have one, yet, register for your free trial here and register your mobile phone as a device following this guide. It’s done in no time!

I also assume that you have some basic understanding of the Python programming language, running Python applications and the Pip package manager.

Also the Cumulocity Python API is closely aligned to the Cumulocity REST API. So, a fair understanding of RESTful API is definitely helpful, but not a requirement.

A brief introduction into the Cumulocity Python API

The Cumulocity Python API library aims to be the standard Python library for Cumulocity IoT application development. It is intended to be used for standalone application and microservices.

The API was designed with ease-of-use and performance in mind:

  • It is pythonic, using standard Python data types, libraries, conventions and idioms. Python developers will find their way in a breeze.
  • It is fully documented including all function parameters which enables full code completion in the programming IDE of your choice.
  • It encapsulates all Cumulocity IoT low level connectivity, takes care of authentication, session handling and payload parsing/formatting.
  • It automatically applies common performance optimizations like pagination, lazy, on demand parsing/formatting and payload minimization.

For this API we decided against creating a full Python abstraction of the Cumulocity IoT functional, data and access model. Instead, the API is closely aligned to the concepts of the standard Cumulocity REST API. We won’t hide the data model details, won’t perform additional requests in the background, and alike.
So, while working with the Cumulocity Python API you will also understand how to work with the standard REST API. You will always know exactly what’s going on. You will always be able to perform a direct REST query for edge cases - the API provides a set of nice access points just for that.

The Cumulocity Python API is not an official API provisioned by Software AG. It’s an open source project (hosted on GitHub), maintained by Software AG consultants and other Cumulocity experts world wide. You are invited to contribute!

Getting started - project setup

Getting started we want to create an application which connects to a Cumulocity IoT tenant and lists the connected devices with owner details. The API was designed with Docker on Cumulocity in mind, but creating a stand-alone application is just as easy.

We will call the application/project firstApp - feel free to choose your own catchy name and be sure to change the listings below accordingly.

$ mkdir -p firstApp/src
$ cd firstApp

Create a virtual environment and get the c8y-API library

I recommend using a virtual environment by default, you can do without but there is no reason not to. If you are new to Python and virtual environments have a look here: Python Virtual Environments: A Primer

$ python3 -m venv venv            # create virtual environment
$ source venv/bin/activate        # step into the virtual environment
(venv) pip3 install c8y-api       # install the Cumulocity Python API

The last command will download the latest version of the Cumulocity Python API (the library is abbreviated c8y-api) including all dependencies from pypi.org and install it into the previously created virtual environment (indicated by the (venv) prompt).

Now we are all set to start developing!

Connecting to the Cumulocity tenant

We will start by creating our main Python script, e.g. src/app.py in an code editor of your choice. For this guide we won’t need anything more sophisticated.

Our entry point to the world of Cumulocity IoT is through the CumulocityApi class which can be imported from the c8y_api library using

from c8y_api import CumulocityApi

c8y = CumulocityApi(base_url='',    # the url of your Cumulocity tenant here
                    tenant_id='',   # the tenant ID of your Cumulocity tenant here
                    username='',    # your Cumulocity IoT username
                    password='')    # your Cumulocity IoT password

The CumulocityApi class can be initialized with all necessary connection and authentication details for your Cumulocity IoT tenant. Don’t worry! There are more advanced, enterprise-ready methods to provide this information, but for now this one is a lot easier to use.

First action!

In this first application we will simply iterate through all registered devices and list their Cumulocity object ID, designation and owner:

for d in c8y.device_inventory.select():
    print(f"Found device #{d.id} '{d.name}', owned by {d.owner}")

Let’s have a look at this in detail. You can see that access to the device inventory is provided through the device_inventory property of the CumulocityApi class. Likewise, it provides access to events, alarms, managed objects and all other aspects of the Cumulocity information model. Feel free to explore!

Looping through objects is provided through the select function. This function features many parameters, primarily to specify selection filters. We don’t worry about these right now because we simply want to see everything. Internally, this function sends a GET request to the /inventory/managedObjects endpoint, parses the result, and produces corresponding Python objects.

The return of the select function is a series of Device instances that you can work with directly. In this example we simply print the Cumulocity IoT object ID, the device name and the device owner. All given properties of a Device object in Cumulocity are represented as corresponding class properties in Python. And - as the result of the select function is typed - code completion works as well.

This is it! Assuming that you are in the project base folder and you’ve put your code into file src/app.py you can run your first application by

(venv) python src/app.py

This outputs the metadata of all registered devices onto the console.

Creating an alarm

Now, we will start changing things. If you already have some experience with Cumulocity IoT you might know about its flexible information model. We will make use of it by creating an alarm with custom fragments.

An alarm can be created by creating an Alarm object and posting it. The Alarm class can be imported from the c8y_api.model package. We also import the standard datetime class to time the alarm properly:

from c8y_api.model import Alarm
from datetime import datetime, timezone

To raise an alarm for a specific device, we need the Cumulocity IoT object ID of one of the registered devices. Luckily, we just printed all of them in the previous section. You might just pick one of them by updating this line:

device_id = '' # your device ID needs to be inserted here

You can also just pick ID of the last device listed before like this:

device_id = d.id  # d is still in memory from the loop

The Alarm class’ constructor features named parameters for the Alarm’s core properties like type and time. Please note that we specify the device by pushing the previously copied Cumulocity object ID into the source parameter.

alarm_time = datetime.now(timezone.utc)

test_alarm = Alarm(type='cx_TestAlarm',
                   time=alarm_time,
                   source=device_id,
                   text=f"Test alarm at {alarm_time}",
                   severity=Alarm.Severity.WARNING)

c8y.alarms.create(test_alarm)

After instantiation, the object is then inserted into Cumulocity IoT using the create function which is one of many held at the alarms property of the CumulocityApi instance we previously set up.

Go ahead an run our changes. you won’t see any additional output, but you should now be able to locate the created API within the Cumulocity IoT web interface.

Custom fragments

Let’s extend this scenario a bit. As previously said, Cumulocity IoT features a very flexible information model - virtually any JSON structure can be attached to any database object as custom fragments (see also: Cumulocity IoT’s domain model).

Likewise, we can simply provide additional properties as custom fragments after the standard parameters in the Cumulocity Python API:

test_alarm = Alarm(type='cx_TestAlarm',
                   time=alarm_time,
                   source=device_id,
                   text=f"Test alarm at {alarm_time}",
                   severity=Alarm.Severity.WARNING,
                   # custom fragments below
                   cx_CustomData={'foo': 'bar', 'data': {'is_important': True}})

Here, we added a fragment named cx_CustomData with some random data in it. As you can see, you can provide any JSON structure here.

Alternatively you can add such fragments after object instantiation using the [] operator:

test_alarm['cx_MoreData'] = {'nice': True}

Once these fragments are present, you can easily access them using standard Python notation:

test_alarm['cx_CustomData']['foo']          # access using [] notation
test_alarm.cx_CustomData.data.is_important  # access using dot notation

Let’s loop through all alarms and list their details:

for a in c8y.alarms.select(source=device_id):
    print(f"Found alarm #{a.id}, {a.text}, fragments: {list(a.keys())}")
    if 'cx_CustomData' in a:
        print(f"   Important: {a.cx_CustomData.data.is_important}")
        print(f"   More data: {a['cx_CustomData']['foo']}")

Like before, when we looped through the devices, we use a select function to loop through objects. Note that we are working with the alarms instead of the device_inventory resource this time. The Cumulocity Python API defines multiple of these resources that all behave in the same way.

You can see a lot of additional features of the API as well. First of all, we introduced a filter: we only select alarms that are assigned to our device using the source parameter. When exploring Alarm objects we can work with fragments using standard Python notation: We use the keys() function to list custom fragments, the in operator to check for specific fragments and the [] operator as well as dot notation to address specific properties of these fragments.

You can run this application again. You will see additional output that lists all alarms (the just created and any previous ones), including the custom fragments.

Clearing alarms

Within Cumulocity IoT, alarms are special events that need manual intervention. They feature a lifecycle and correspondingly can only be created once (creating an identical alarm multiple times does not raise the alarm again, see also Cumulocity IoT’s Event model.

Because of this, we can run our sample application multiple times without spamming the platform with additional alarms. A alarm can only be raised (i.e. created) again, when it was previously acknowledged and cleared. We can do this in the UI (feel free to do that right now!) or we can do this using Python.

Updating via the Cumulocity Python API is particularly easy. Let ups loop through all alarms of our device and clear them:

for a in c8y.alarms.select(source=device_id, status=Alarm.Status.ACTIVE):
    a.status = Alarm.Status.CLEARED
    a.update()
    print(f"Alarm #{a.id} cleared.")

Like before we use the select function to loop over the alarms. This time, we add another filter for the alarm’s status - no need to visit inactive alarms.

To update an alarm we simply update the status property of the instance and invoke the update function. Internally this will create a POST request which will push the changes (the status update) to Cumulocity IoT.

Invoking the update function directly on the Alarm instance is what we call the object-oriented invocation style. In fact, if you prefer differently you can also invoke the update function functional style on the CumulocityApi instance with the same result.

c8y.alarms.update(a)   # this would work as well

You can now run the application over and over again. It will

  • first list all devices,
  • then create an alarm for one of them,
  • list all already created alarms
  • acknowledge all open alarms

A note to pro-users

You might think that you could have updated our alarm directly without the loop like this:

test_alarm.status = Alarm.Status.CLEARED`
test_alarm.update()   # this does not work

This won’t work. Why? Well, in the end the update function of the Alarm class needs to send a POST request towards Cumulocity IoT. To be able to do that it needs to have access to a valid connection. We haven’t specified that. Also, to update an object you need the Cumulocity IoT object ID (of the alarm). We haven’t specified that either.

If you know the ID of the alarm object, you could do that, though:

test_alarm.c8y = c8y   # specify cumulocity connection
test_alarm.id  = ''    # specify the alarm object id

test_alarm.status = Alarm.Status.CLEARED`
test_alarm.update()    # this would work now

Ok, but wait! Why does the very same then work within the loop? Well, because both - the connection reference and the object ID - are injected into the Alarm instances generated by the select function automatically. Neat, right?

Where to next?

I hope you had fun following this quick start guide and you got interested in learning more. Please feel free to experiment! I hope I was able to show that the Cumulocity Python API makes development for Cumulocity IoT as easy as it can be.

Some hints where to go next:

  • Build your own metadata using the Cumulocity inventory. The Cumulocity Python API
    makes handling custom fragments particularly easy!
  • Have a look at measurements! You can use the API to easily grab measurements of a
    specific types, timeframes and other characteristics. You can also create measurements using a neat object-oriented API

In the near future we will create additional guides for more advanced topics like authentication and authorization, dockerization, device registration, etc.

Finally, I’d like to thank everybody who already participated in the development of the Cumulocity Python API: you brought this to life, thanks!
If you are interested in participating as well, please join our GitHub community.

Full listing src/app.py

from c8y_api import CumulocityApi
from c8y_api.model import Alarm
from datetime import datetime, timezone


c8y = CumulocityApi(base_url='',    # the url of your Cumulocity tenant here
                    tenant_id='',   # the tenant ID of your Cumulocity tenant here
                    username='',    # your Cumulocity IoT username
                    password='')    # your Cumulocity IoT password

for d in c8y.device_inventory.select():
    print(f"Found device #{d.id} '{d.name}', owned by {d.owner}")

device_id = ''  # your device ID needs to be inserted here
# uncomment next line to simply use the last device listed
# device_id = d.id  
alarm_time = datetime.now(timezone.utc)

test_alarm = Alarm(type='cx_TestAlarm',
                   time=alarm_time,
                   source=device_id,
                   text=f"Test alarm at {alarm_time}",
                   severity=Alarm.Severity.WARNING,
                   # custom fragments below
                   cx_CustomData={'foo': 'bar', 'data': {'is_important': True}})

test_alarm['cx_MoreData'] = {'nice': True}

c8y.alarms.create(test_alarm)

# listing alarms
for a in c8y.alarms.select(source=device_id):
    print(f"Found alarm #{a.id}, {a.text}, fragments: {list(a.keys())}")
    if 'cx_CustomData' in a:
        print(f"   Important: {a.cx_CustomData.data.is_important}")
        print(f"   More data: {a['cx_CustomData']['foo']}")

# clearing alarms
for a in c8y.alarms.select(source=device_id, status=Alarm.Status.ACTIVE):
    a.status = Alarm.Status.CLEARED
    a.update()
    # c8y.alarms.update(a)   # this would work as well
    print(f"Alarm #{a.id} cleared.")

This article is part of the TECHniques newsletter blog - technical tips and tricks for the Software AG community. Subscribe to receive our quarterly updates or read the latest issue.

9 Likes