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

Cumulocity IoT Web Development Tutorial - Part 1: Start your journey
Cumulocity IoT Web Development Tutorial - Part 2: Create your first component
Cumulocity IoT Web Development Tutorial - Part 3: Query data from Cumulocity IoT
Cumulocity IoT Web Development Tutorial - Part 4: Convert component into a widget
Cumulocity IoT Web Development Tutorial - Part 5: Provide widget as a UI Plugin

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:

image

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.

Ngx-components provides a couple on convenience services, you can use to easily load and subscribe for real-time data: MeasurementRealtimeService, AlarmRealtimeService or ManagedObjectRealtimeService. You want to use MeasurementRealtimeService to subscribe for real-time measurements. Looking at the documentation, you can see it provides a set of functions, which subscribe to real-time measurments based on specified criteria. For example, you can listen to all measurements or limit it to a specific device by using onAll$(). Or you could subcribe to real-time updates for specific measurements, once they have been created, using onCreateOfSpecificMeasurement$. In your case, you will use the function latestValueOfSpecificMeasurement$, which returns an Observable. The Observable provides you with the latest measurement and at the same time subscribes for real-time updates to receive new measurements:

device-info.service.ts

import { get } from 'lodash';
import { MeasurementRealtimeService } from '@c8y/ngx-components';

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

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

  private async loadLatestMeasurement(
    deviceId: string,
    measurementFragment: string,
    measurementSeries: string
  ) {
    try {
      this.measurementRealtimeService
        .latestValueOfSpecificMeasurement$(measurementFragment, measurementSeries, deviceId)
        .subscribe((measurement) => {
          const temperatureValue: number = get(
            measurement,
            `${measurementFragment}.${measurementSeries}.value`
          );
          const temperatureUnit: string = get(
            measurement,
            `${measurementFragment}.${measurementSeries}.unit`
          );

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

The loadLatestMeasurement$ function takes three parameters. The deviceId is used to specify for which device measurement updates should be received. measurementFragment and measurementSeries are necessary to declare the measurement for which you want to receive updates. All three parameters are used for latestValueOfSpecificMeasurement$() to get an Observable to which you will subscribe. Once subscribed, you will get the latest measurement and all measurements created going forward.

The received measurement will be processed to extract the value and the unit from it. You will use the get helper function from lodash to read the value and unit of the temperature measurement. Once done, 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);

Lastly, you need to add the MeasurementRealtimeService to the providers section in the DeviceInfoModule:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CoreModule, MeasurementRealtimeService, hookNavigator } from '@c8y/ngx-components';

import { DeviceInfoComponent } from './device-info.component';
import { DeviceInfoNavigationFactory } from './device-info.factory';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'device-info',
    pathMatch: 'full',
  },
  {
    path: 'device-info',
    component: DeviceInfoComponent,
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes), CoreModule],
  exports: [],
  declarations: [DeviceInfoComponent],
  providers: [MeasurementRealtimeService, hookNavigator(DeviceInfoNavigationFactory)],
})
export class DeviceInfoModule {}

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

thumbs_up

Unsubscribe from real-time notifications

If you have subscribed for real-time measurement, you want to make sure to unsubscribe from these real-time notifications again if the corresponding component gets destroyed to free up resources and to avoid any leaks.

Extend the DeviceInfoService with the functionality to unsubscribe from an existing real-time subscription.

device-info.service.ts

@Injectable()
export class DeviceInfoService {
   ...

   private realtimeSubscription: Subscription;

   ...

   unscubscribeFromTemperatureMeasurements(): void {
    if (!this.realtimeSubscription) {
      return;
    }

    this.realtimeSubscription.unsubscribe();
  }

  ...

  private async loadLatestMeasurement(
    deviceId: string,
    measurementFragment: string,
    measurementSeries: string
  ) {
    try {
      this.realtimeSubscription = this.measurementRealtimeService
        .latestValueOfSpecificMeasurement$(measurementFragment, measurementSeries, deviceId)
        .subscribe((measurement) => {
          const temperatureValue: number = get(
            measurement,
            `${measurementFragment}.${measurementSeries}.value`
          );
          const temperatureUnit: string = get(
            measurement,
            `${measurementFragment}.${measurementSeries}.unit`
          );

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

A new variable, called realtimeSubscription, has been introduced. This variable is used to store the subscription. The assignment of an active subscription is done in the loadLatestMeasurement method: this.realtimeSubscription = this.measurementRealtimeService.latestValueOfSpecificMeasurement$(). Furthermore, a new method unscubscribeFromTemperatureMeasurements() has been added to the service, which allows you to unsubscribe from real-time measurements, in case a subscriptions exists.

The method unscubscribeFromTemperatureMeasurements() will be used by the corresponding component DeviceInfoComponent.

device-info.component.ts

import { Component, OnDestroy, OnInit } from '@angular/core';

...

@Component({
  selector: 'c8y-device-info',
  templateUrl: 'device-info.component.html',
  providers: [DeviceInfoService],
})
export class DeviceInfoComponent implements OnInit, OnDestroy {
  ...

  ngOnDestroy(): void {
    this.unsubscribeForTemperatureMeasurements();
  }

  ...

  private unsubscribeForTemperatureMeasurements() {
    this.deviceInfoService.unscubscribeFromTemperatureMeasurements();
  }
}

Add a new method to the DeviceInfoComponent, called unsubscribeForTemperatureMeasurements(). This method calls the unscubscribeFromTemperatureMeasurements() from the DeviceInfoService. To call this new method, if the component gets destroyed, you need to implement the OnDestroy interface for your DeviceInfoComponent. Implement the ngOnDestroy and call the unsubscribeForTemperatureMeasurements of your component. This means, if your component is destroyed by the application, the real-time subscription established by your component will be removed as well.

If you run your project locally again, you will see temperature measurements being updated continuously because of the real-time subscription:

device-info-realtime

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 this tutorial series you will learn how to add Cumulocity dashboards to your application. You will convert the device-info plugin into a widget, to actually use it on a Cumulocity dashboard.

6 Likes

Hi,
Missing this line in the section “Get latest measurement”:
import { InventoryService, MeasurementService } from '@c8y/client';
Besides that, and once I found that I had to create a secondary non-cloud user to login to this app, your tutorial works great ! Thanks for the detailed steps :wink:

1 Like

Hi Olivier,

thanks for the feedback. Much appreciated. I have added the missing statement for the import of the relevant services.