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!
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/websdk
package (which you have installed in the first part of this tutorial) adds the @c8y/devkit package as a dev dependency to the project. The @c8y/devkit
package provides custom builders to build and deploy a Cumulocity project. To build your application, you need to run this instruction at the root of your project:
ng 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:
ng 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.
$ ng deploy
? Enter tenant username: {{your-username}}
? Enter tenant password: [hidden] ************
? Enter tenant url: {{c8y-instance}}
âś” Browser application bundle generation complete.
âś” Copying assets complete.
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 forApplication 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 theAdmin
permission on theSimulator
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 | undefined> {
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.
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 { 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
): WritableSignal<TemperatureMeasuerement | undefined> {
this.loadLatestMeasurement(
deviceId,
this.TEMPERATURE_FRAGMENT,
this.TEMPERATURE_SERIES
);
return this.temperatureMeasurement;
}
private async loadLatestMeasurement(
deviceId: string,
measurementFragment: string,
measurementSeries: string
) {
try {
this.realtimeSubscription = this.measurementRealtimeService
.latestValueOfSpecificMeasurement$(
measurementFragment,
measurementSeries,
deviceId
)
.subscribe((measurement) => {
this.temperatureMeasurement.set({
value: measurement[measurementFragment][measurementSeries]['value'],
unit: measurement[measurementFragment][measurementSeries]['unit'],
});
});
} 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. Once done, use the temperatureMeasurement: WritableSignal<TemperatureMeasuerement | undefined>
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. In addition, return the temperatureMeasurement
signal. 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.tempteratureMeasurement =
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.
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.measurementRealtimeService
.latestValueOfSpecificMeasurement$(
measurementFragment,
measurementSeries,
deviceId
)
.subscribe((measurement) => {
this.temperatureMeasurement.set({
value: measurement[measurementFragment][measurementSeries]['value'],
unit: measurement[measurementFragment][measurementSeries]['unit'],
});
});
} 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:
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.