Cumulocity Web Development Tutorial - Part 3: Query data from Cumulocity

Part 3 has finally arrived!

about-time

In the last part you have implemented your first component, which displays device data and temperature measurements. For now, you have mocked the data. In the third part of the web development tutorial series, you will start to query data directly from Cumulocity IoT. You will create a simulated device, which continuously sends temperature measurements. Your Cumulocity component will subscribe to these temperature measurements and will display the latest temperature measurement received by the platform.

But before, let’s have a brief look at how your current application can be build and deployed to Cumulocity.

Build and deploy a Cumulocity application

The @c8y/cli tools, which you have installed in the first part of this tutorial, have two commands. These commands let you build and deploy your application. To build your application, you need to run this instruction at the root of your project:

npx c8ycli build

This command compiles your application and creates a new directory dist in your project, which contains all relevant files. Once you have build your application, you are ready to deploy it. You can deploy your application by running this command at the root level of your application:

npx c8ycli deploy

The command will start a wizard, which guides you through the deployment process. You need to specify the Cumulocity tenant to which the application should be deployed to along with your username and password.

prompt: Instance URL:  (http://demos.cumulocity.com) https://{{your-tenant}}.cumulocity.com
prompt: Username:  (admin) {{your-username}}
prompt: Password: ********
Fetching application list...
Creating application...
Uploading application zip with 4093951 bytes...
Activating binary version id 8001643...
Application my-c8y-application deployed.

The command will take the content from the dist directory and create a zip archive out of it. This zip archive will be uploaded to the defined Cumulocity tenant. Once uploaded, the application will be activated and becomes available in the Cumulocity tenant.

Important: the account you use for deploying the application needs to have Admin permissions for Application management.

Create a simulated temperature sensor

Now that you have learned how to build and deploy an application, you will learn how to query data from Cumulocity IoT using its different APIs. In the previous part, you have displayed device data and its temperature measurement. For now, this data has been mocked within the project and the corresponding component. To learn how to actually query data directly from Cumulocity IoT, you will create a simulated device in Cumulocity, which mimics a real device. This simulated device publishes temperature measurements in an endless loop. The mocked data in the application will be replaced with actual data coming from the simulated temperature sensor.

Let’s create the simulator. In the official documentation, you will find all the necessary steps to create a new simulator. Read the documentation carefully.

Important: You might see an error, if you try to access the Simulators section in the Device Management application. In case you experience this error, make sure your user or assigned roles have the Admin permission on the Simulator API set.

Instead of creating a simulator from scratch, you will simply use one of the predefined simulators. In the Simulators section of the Device Management application, click on Add simulator. Select Temperature measurements in the Presets list. Provide a meaningful name for your simulator, such as Temperature sensor. Keep the number of instance equal to 1. Finally, click on Create to generate your new simulator.

Your newly created simulated temperature sensor consists of 60 instructions. It creates temperature measurements, which have following structure:

{
	"c8y_Temperature": {
    	"T": { 
        	"value": 25,
            "unit": "°C" }
        },
    "time":"{{timestamp}}", 
    "source": {
    	"id":"{{deviceId}}" }, 
    "type": "c8y_Temperature"
}

In the overview of the simulators click on the toggle button of your simulator to start it. Once you started your simulator, a new device instance is created in your Cumulocity tenant. Check the All devices section in your Device Management application and you will see a new device, which is your simulated temperature sensor:

This will be the device you will query the data for in your Cumulocity web application. If you click on your device and have a look at the Measurements section, you can see actual temperature measurements being generated by the simulator.

Query data using @c8y/client

The Cumulocity Web SDK comes with the @c8y/client package. Additional documentation is available here. The @c8y/client is isomorphic and therefore can be used standalone in other web projects independent of their technology. It consists of several services to send requests to Cumulocity’s APIs, e.g. the Inventory API for Managed Objects or the Event API for events. It’s using JavaScript Promises for asynchronous communication. In this section, you will learn how to use the @c8y/client to query data from the Inventory and how to subscribe for real-time measurements.

Get data from the Inventory

You can continue with the project from the 2nd part of this tutorial series. First, you will replace the mocked device data in the DeviceInfoService and query the actual device data from the Inventory. Inject the InventoryService into your DeviceInfoService.

import { InventoryService } from '@c8y/client';

@Injectable()
export class DeviceInfoService {
  ...
  constructor(private inventoryService: InventoryService) {}
  ...
}

Update the getDeviceDetails() to request device details from the Inventory instead of mocking the data. Add a parameter for the device id to the function. This device id is used as input for the InventoryService function detail(), which returns the corresponding ManagedObject for the provided id.

async getDeviceDetails(deviceId: string): Promise<DeviceDetails> {
  try {
    const response = await this.inventoryService.detail(deviceId);
    const deviceManagedObject = response.data;

    return { name: deviceManagedObject.name, type: deviceManagedObject.type };
  } catch (error) {
    console.error('Error occurred while loading the device description: ', error);

    return undefined;
  }
}

Surround the request to Cumulocity’s API with a try/catch to create a log statement and return undefined in case of any error, e.g. if the ManagedObject couldn’t be found in the Inventory. Use the name and type field of the ManagedObject to return an object, which implements the DeviceDetails interface.

Now you need to update the getDeviceDetails() call in the corresponding DeviceInfoComponent. First create a new constant in the component to specify the device id of your simulated temperature sensor. You can find the id of your device on its detail page in Cumulocity:

Use the constant as the input for the call of the getDeviceDetails() function.

@Component({
  selector: 'c8y-device-info',
  templateUrl: 'device-info.component.html',
  providers: [DeviceInfoService],
})
export class DeviceInfoComponent implements OnInit {
  private readonly DEVICE_ID = '{{deviceId}}';
  ...
  private async initDeviceDetails() {
    this.deviceDetails = await this.deviceInfoService.getDeviceDetails(this.DEVICE_ID);
  }
  ...
}

Replace {{deviceId}} with your device id. If you start your application locally, you will see that the component queries device data from your Cumulocity tenant:

Get latest measurement

Now, you want to update how measurements are loaded from Cumulocity. For this, you need to fetch the latest measurement available for your device and in parallel subscribe for real-time notifications to receive updates for temperature measurements.

To query the latest measurement, you need to use the MeasurementService and its list() function. The list() function takes a filter object as a parameter. Getting the latest measurement is a bit tricky as you need to specify the revert query parameter, which only works if you set the dateFrom and dateTo query parameters. In the DeviceInfoService create a new private function to query the latest measurement:

device-info.service.ts

import { has, get } from 'lodash';

@Injectable()
export class DeviceInfoService {
  private readonly TEMPERATURE_FRAGMENT = 'c8y_Temperature';

  private readonly TEMPERATURE_SERIES = 'T';
  ...
  constructor(
    private inventoryService: InventoryService,
    private measurementService: MeasurementService
  ) {}
  
  subscribeForTemperatureMeasurements(deviceId: string): void {
    this.loadLatestMeasurement(deviceId, this.TEMPERATURE_FRAGMENT, this.TEMPERATURE_SERIES);
  }

  private async loadLatestMeasurement(
    deviceId: string,
    measurementFragment: string,
    measurementSeries: string
  ) {
    const filter = {
      source: deviceId,
      dateFrom: '1970-01-01',
      dateTo: new Date().toISOString(),
      valueFragmentType: measurementFragment,
      valueFragmentSeries: measurementSeries,
      pageSize: 1,
      revert: true,
    };

    try {
      const response = await this.measurementService.list(filter);

      if (
        !response.data ||
        response.data.length != 1 ||
        !has(response.data[0], `${measurementFragment}.${measurementSeries}`)
      ) {
        return;
      }

      const temperatureValue: number = get(
        response.data[0],
        `${measurementFragment}.${measurementSeries}.value`
      );
      const temperatureUnit: string = get(
        response.data[0],
        `${measurementFragment}.${measurementSeries}.unit`
      );

      this.temperatureMeasurement$.next({ value: temperatureValue, unit: temperatureUnit });
    } catch (error) {
      console.error('Error occurred while loading the latest measurement: ', error);
    }
  }
}

The important part of this function is the filter object, which takes measurementFragment and measurementSeries to set the valueFragmentType and valueFragmentSeries as query parameters. Furthermore, it defines the dateFrom and dateTo, which is a requirement for the revert query parameter to work. The revert parameter is set to true. As only the latest measurement should be loaded, the pageSize is set to 1. The device id will be set as source query parameter. The filter object is input for the this.measurementService.list(filter) call.

The response received will be processed to extract the value and the unit for the measurement. You will use some helper functions from lodash to check if the measurement fragment and series are present. This check is done using the has function of lodash. If the fragment and series are available you will use the get function to read the value and unit of the temperature measurement. Use the temperatureMeasurement$: Subject<TemperatureMeasuerement> to publish the latest measurement to the component.

Update the subscribeForTemperatureMeasurements function to take the deviceId as parameter. Remove the old code with the mocked data and instead call the loadLatestMeasurement function. As input provide the deviceId, fragment and series. The fragment and series of the temperature measurement are defined as constants in the DeviceInfoService:

device-info.service.ts

...
private readonly TEMPERATURE_FRAGMENT = 'c8y_Temperature';

private readonly TEMPERATURE_SERIES = 'T';
...

Important If you use your own device or custom simulator, which sends different measurements, you might need to adjust the fragment and series above based on the structure of your measurement, you want to subscribe to.

You can remove the getRandomInt(min, max) function in the DeviceInfoService. This function isn’t needed anymore.

In the DeviceInfoComponent update the call of the subscribeForTemperatureMeasurements by providing the device id as a parameter:

device-info.component.ts

this.deviceInfoService.subscribeForTemperatureMeasurements(this.DEVICE_ID);

Subscribe for real-time notifications

Last but not least, you want to subscribe for real-time notifications to continuously receive the latest measurements. The generic approach is to use the Realtime service of the @c8y/client. The Realtime service allows you to subscribe for any real-time notifications, e.g. alarms or measurements. You need to to use the [subscribe()] function to register for real-time notifications. Based on the subscribed channel, you will receive the according notifications in the callback, which you have defined. In this case you want to register for measurement updates and therefore you subscribe to the channel /measurements/{{deviceId}}. The placeholder {{deviceId}} needs to be replaced with the id of the device, you want to the receive measurements for. In case you want to get the measurements for all devices within your tenant, you can use the asterisk character *, instead of specifying a device id.

Update the DeviceInfoService. Inject the Realtime service and add a new function to subscribe for measurement updates.

device-info.service.ts

import { IMeasurement, InventoryService, MeasurementService, Realtime } from '@c8y/client';

export class DeviceInfoService {
  ...
  constructor(
    private inventoryService: InventoryService,
    private measurementService: MeasurementService,
    private realtime: Realtime
  ) {}
  ...
  private subscribeForMeasurements(
    deviceId: string,
    measurementFragment: string,
    measurementSeries: string
  ) {
    this.realtime.subscribe(`/measurements/${deviceId}`, (measurementNotification) => {
      const measurement: IMeasurement = measurementNotification.data.data;
      if (!measurement || !has(measurement, `${measurementFragment}.${measurementSeries}`)) {
        return;
      }

      const temperatureValue: number = get(
        measurement,
        `${measurementFragment}.${measurementSeries}.value`
      );
      const temperatureUnit: string = get(
        measurement,
        `${measurementFragment}.${measurementSeries}.unit`
      );
      this.temperatureMeasurement$.next({ value: temperatureValue, unit: temperatureUnit });
    });
  }

The subscribeForMeasurements() function takes the id of the device, the fragment and the series of the measurement as a parameter. The id of the device is used for the subscribe() function of the Realtime service to only receive notifications for the this particular device. Once a notification has been received it will be converted to a measurement. Based on the fragment and series, which have bee provided as parameters, the corresponding value and unit of the measurement are extracted and written to the temperatureMeasurement$ Subject.

Finally, you need to update the subscribeForTemperatureMeasurements() in the DeviceInfoService to call the subscribeForMeasurements() function.

subscribeForTemperatureMeasurements(deviceId: string): void {
  this.loadLatestMeasurement(deviceId, this.TEMPERATURE_FRAGMENT, this.TEMPERATURE_SERIES);

  this.subscribeForMeasurements(deviceId, this.TEMPERATURE_FRAGMENT, this.TEMPERATURE_SERIES);
}

With these enhancements, you can now receive and display actual measurements of your device from Cumulocity IoT in your application.

thumbs_up

Conclusion and what’s next

You can find the code of the updated application of part 03 in the corresponding github repository. Make sure to replace {{deviceId}} in the device-info.component.ts to specify your device id. Furthermore, if you want to run the full sample locally, make sure to specify your Cumulocity instance in the package.json for the start script.

In the next part of the this tutorial series you will learn how to clone and extend existing applications, such as the Cockpit or Device Management application. You will convert the device-info plugin into a widget, to actually use it on a Cumulocity dashboard.

4 Likes