Introduction

ThingsBoard widgets are additional UI modules that easily integrate into any IoT Dashboard. They provide end-user functions such as data visualization, remote device control, alarms management and display of static custom html content. According to the provided features, each widget definition represents a specific Widget Type.

Creating new widget definition

In order to create a new widget definition, navigate to “Widget Library” and open existing “Widgets Bundle” or create a new one. In the “Widgets Bundle” view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button.
部件Widgets3.x开发 - 图1
“Select widget type” window should appear with select options corresponding to the widget type you intend to develop.
部件Widgets3.x开发 - 图2
After that, the pre-populated “Widget Editor” page will open with starter widget template according to previously selected widget type.
部件Widgets3.x开发 - 图3

Widget Editor overview

Widget Editor view is a mini IDE designed to develop custom widget definitions. It consists of top toolbar and four main sections:

  • Resources/HTML/CSS
  • JavaScript
  • Settings schema
  • Widget preview

    Widget Editor Toolbar

    部件Widgets3.x开发 - 图4
    Widget Editor Toolbar consists of the following items:

  • Widget Title field - used to specify title of the widget definition

  • Widget Type selector - used to specify type of the widget definition
  • Run button - used to run widget code and view result in Widget preview section
  • Undo button - reverts all editor sections to latest saved state
  • Save button - saves widget definition
  • Save as button - allows to save a new copy of widget definition by specifying new widget type name and target Widgets Bundle

    Resources/HTML/CSS section

    This section consists of three tabs:
    The first Resources tab is used to specify external JavaScript/CSS resources used by the widget.
    部件Widgets3.x开发 - 图5
    Second HTML tab contains widget html code (Note: some widgets create html content dynamically, thus their initial html content can be empty).
    部件Widgets3.x开发 - 图6
    Third CSS tab contains widget specific CSS style definitions.
    部件Widgets3.x开发 - 图7

    JavaScript section

    This section contains all widget related JavaScript code according to the Widget API.
    部件Widgets3.x开发 - 图8

    Settings schema section

    This section consists of two tabs:
    The first tab, Settings schema, is used to specify the json schema of widget settings for UI form auto-generation using react-schema-form builder. This generated UI form is displayed in the Advanced tab of widget settings. The Settings Object serialized by this schema is used to store specific widget settings and is accessible from widget JavaScript code.
    部件Widgets3.x开发 - 图9
    The second tab, Data key settings schema, is used to specify json schema of data key settings for UI form auto-generation using react-schema-form builder. This generated UI form is displayed in Advanced tab of the Data key configuration dialog. The Settings Object serialized by this schema is used to store specific settings for each data key of the datasource defined in the widget. These settings are accessible from widget JavaScript code.
    部件Widgets3.x开发 - 图10

    Widget preview section

    This section is used to preview and test widget definitions. It is presented as a mini dashboard containing one widget instantiated from the current widget definition. It has mostly all functionality provided by usual ThingsBoard dashboard, with some limitations. For example, “Function” can only be selected as datasource type in widget datasources section for debugging purposes.
    部件Widgets3.x开发 - 图11

    Basic widget API

    All widget related code is located in the JavaScript section. The built-in variable self that is a reference to the widget instance is also available. Each widget function should be defined as a property of the self variable. self variable has property ctx of type WidgetContext - a reference to widget context that has all necessary API and data used by widget instance. Below is brief description of widget context properties:
Property Type Description
$container jQuery Object Container element of the widget. Can be used to dynamically access or modify widget DOM using jQuery API.
$scope IDynamicWidgetComponent Reference to the current widget component. Can be used to access/modify component properties when widget is built using Angular approach.
width Number Current width of widget container in pixels.
height Number Current height of widget container in pixels.
isEdit Boolean Indicates whether the dashboard is in in the view or editing state.
isMobile Boolean Indicates whether the dashboard view is less then 960px width (default mobile breakpoint).
widgetConfig WidgetConfig Common widget configuration containing properties such as color (text color), backgroundColor (widget background color), etc.
settings Object Widget settings containing widget specific properties according to the defined settings json schema
datasources Array[Datasource](https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L250) Array of resolved widget datasources. See Subscription object.
data Array[DatasourceData](https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L275) Array of latest datasources data. See Subscription object.
timeWindow WidgetTimewindow Current widget timewindow (applicable for timeseries widgets). Holds information about current timewindow bounds. minTime - minimum time in UTC milliseconds, maxTime - maximum time in UTC milliseconds, interval - current aggregation interval in milliseconds.
units String Optional property defining units text of values displayed by widget. Useful for simple widgets like cards or gauges.
decimals Number Optional property defining how many positions should be used to display decimal part of the value number.
hideTitlePanel Boolean Manages visibility of widget title panel. Useful for widget with custom title panels or different states. updateWidgetParams() function must be called after this property change.
widgetTitle String If set, will override configured widget title text. updateWidgetParams() function must be called after this property change.
detectChanges() Function Trigger change detection for current widget. Must be invoked when widget HTML template bindings should be updated due to widget data changes.
updateWidgetParams() Function Updates widget with runtime set properties such as widgetTitle, hideTitlePanel, etc. Must be invoked in order these properties changes take effect.
defaultSubscription IWidgetSubscription Default widget subscription object contains all subscription information, including current data, according to the widget type. See Subscription object.
timewindowFunctions TimewindowFunctions Object with timewindow functions used to manage widget data time frame. Can by used by Time-series or Alarm widgets. See Timewindow functions.
controlApi RpcApi Object that provides API functions for RPC (Control) widgets. See Control API.
actionsApi WidgetActionsApi Set of API functions to work with user defined actions. See Actions API.
stateController IStateController Reference to Dashboard state controller, providing API to manage current dashboard state. See State Controller.

In order to implement a new widget, the following JavaScript functions should be defined (Note: each function is optional and can be implemented according to widget specific behaviour):

Function Description
onInit() The first function which is called when widget is ready for initialization. Should be used to prepare widget DOM, process widget settings and initial subscription information.
onDataUpdated() Called when the new data is available from the widget subscription. Latest data can be accessed from the defaultSubscription object of widget context (ctx).
onResize() Called when widget container is resized. Latest width and height can be obtained from widget context (ctx).
onEditModeChanged() Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of ctx.
onMobileModeChanged() Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of ctx.
onDestroy() Called when widget element is destroyed. Should be used to cleanup all resources if necessary.
getSettingsSchema() Optional function returning widget settings schema json as alternative to Settings tab of Settings schema section.
getDataKeySettingsSchema() Optional function returning particular data key settings schema json as alternative to Data key settings schema tab of Settings schema section.
typeParameters() Returns WidgetTypeParameters object describing widget datasource parameters. See Type parameters object.
actionSources() Returns map describing available widget action sources (WidgetActionSource) used to define user actions. See Action sources object.

Subscription object

The widget subscription object is instance of IWidgetSubscription and contains all subscription information, including current data, according to the widget type. Depending on widget type, subscription object provides different data structures. For Latest values and Time-series widget types, it provides the following properties:

  • datasources - array of datasources (Array[Datasource](https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L250)) used by this subscription, using the following structure:

    1. datasources = [
    2. { // datasource
    3. type: 'entity',// type of the datasource. Can be "function" or "entity"
    4. name: 'name', // name of the datasource (in case of "entity" usually Entity name)
    5. aliasName: 'aliasName', // name of the alias used to resolve this particular datasource Entity
    6. entityName: 'entityName', // name of the Entity used as datasource
    7. entityType: 'DEVICE', // datasource Entity type (for ex. "DEVICE", "ASSET", "TENANT", etc.)
    8. entityId: '943b8cd0-576a-11e7-824c-0b1cb331ec92', // entity identificator presented as string uuid.
    9. dataKeys: [ // array of keys (Array<DataKey>) (attributes or timeseries) of the entity used to fetch data
    10. { // dataKey
    11. name: 'name', // the name of the particular entity attribute/timeseries
    12. type: 'timeseries', // type of the dataKey. Can be "timeseries", "attribute" or "function"
    13. label: 'Sin', // label of the dataKey. Used as display value (for ex. in the widget legend section)
    14. color: '#ffffff', // color of the key. Can be used by widget to set color of the key data (for ex. lines in line chart or segments in the pie chart).
    15. funcBody: "", // only applicable for datasource with type "function" and "function" key type. Defines body of the function to generate simulated data.
    16. settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section".
    17. },
    18. //...
    19. ]
    20. },
    21. //...
    22. ]
  • data - array of latest data (Array[DatasourceData](https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/widget.models.ts#L275)) received in scope of this subscription, using the following structure:

    1. data = [
    2. {
    3. datasource: {}, // datasource object of this data. See datasource structure above.
    4. dataKey: {}, // dataKey for which the data is held. See dataKey structure above.
    5. data: [ // array of data points
    6. [ // data point
    7. 1498150092317, // unix timestamp of datapoint in milliseconds
    8. 1, // value, can be either string, numeric or boolean
    9. ],
    10. //...
    11. ]
    12. },
    13. //...
    14. ]

    For Alarm widget type it provides the following properties:

  • alarmSource - (Datasource) information about entity for which alarms are fetched, using the following structure:

    1. alarmSource = {
    2. type: 'entity',// type of the alarm source. Can be "function" or "entity"
    3. name: 'name', // name of the alarm source (in case of "entity" usually Entity name)
    4. aliasName: 'aliasName', // name of the alias used to resolve this particular alarm source Entity
    5. entityName: 'entityName', // name of the Entity used as alarm source
    6. entityType: 'DEVICE', // alarm source Entity type (for ex. "DEVICE", "ASSET", "TENANT", etc.)
    7. entityId: '943b8cd0-576a-11e7-824c-0b1cb331ec92', // entity identificator presented as string uuid.
    8. dataKeys: [ // array of keys indicating alarm fields used to display alarms data
    9. { // dataKey
    10. name: 'name', // the name of the particular alarm field
    11. type: 'alarm', // type of the dataKey. Only "alarm" in this case.
    12. label: 'Severity', // label of the dataKey. Used as display value (for ex. as a column title in the Alarms table)
    13. color: '#ffffff', // color of the key. Can be used by widget to set color of the key data.
    14. settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section".
    15. },
    16. //...
    17. ]
    18. }
  • alarms - array of alarms (Array[Alarm](https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/shared/models/alarm.models.ts#L88)) received in scope of this subscription, using the following structure:

    1. alarms = [
    2. { // alarm
    3. id: { // alarm id
    4. entityType: "ALARM",
    5. id: "943b8cd0-576a-11e7-824c-0b1cb331ec92"
    6. },
    7. createdTime: 1498150092317, // Alarm created time (unix timestamp)
    8. startTs: 1498150092316, // Alarm started time (unix timestamp)
    9. endTs: 1498563899065, // Alarm end time (unix timestamp)
    10. ackTs: 0, // Time of alarm acknowledgment (unix timestamp)
    11. clearTs: 0, // Time of alarm clear (unix timestamp)
    12. originator: { // Originator - id of entity produced this alarm
    13. entityType: "ASSET",
    14. id: "ceb16a30-4142-11e7-8b30-d5d66714ea5a"
    15. },
    16. originatorName: "Originator Name", // Name of originator entity
    17. type: "Temperature", // Type of the alarm
    18. severity: "CRITICAL", // Severity of the alarm ("CRITICAL", "MAJOR", "MINOR", "WARNING", "INDETERMINATE")
    19. status: "ACTIVE_UNACK", // Status of the alarm
    20. // ("ACTIVE_UNACK" - active unacknowledged,
    21. // "ACTIVE_ACK" - active acknowledged,
    22. // "CLEARED_UNACK" - cleared unacknowledged,
    23. // "CLEARED_ACK" - cleared acknowledged)
    24. details: {} // Alarm details object derived from alarm details json.
    25. }
    26. ]

    For RPC or Static widget types, subscription object is optional and does not contain necessary information.

    Timewindow functions

    Object with timewindow functions (TimewindowFunctions) used to manage widget data time frame. Can by used by Time-series or Alarm widgets.

Function Description
onUpdateTimewindow(startTimeMs, endTimeMs) This function can be used to update current subscription time frame to historical one identified by startTimeMs and endTimeMs arguments.
onResetTimewindow() Resets subscription time frame to default defined by widget timewindow component or dashboard timewindow depending on widget settings.

Control API

Object that provides API functions (RpcApi) for RPC (Control) widgets.

Function Description
sendOneWayCommand(method, params, timeout) Sends one way (without response) RPC command to the device. Returns command execution promise. method - RPC method name, string, params - RPC method params, custom json object, timeout - maximum delay in milliseconds to wait until response/acknowledgement is received.
sendTwoWayCommand(method, params, timeout) Sends two way (with response) RPC command to the device. Returns command execution promise with response body in success callback.

Actions API

Set of API functions (WidgetActionsApi) to work with user defined actions.

Function Description
getActionDescriptors(actionSourceId) Returns the list of action descriptors for provided actionSourceId
handleWidgetAction($event, descriptor, entityId, entityName) Handles action produced by particular action source. $event - event object associated with action, descriptor - action descriptor, entityId and entityName - current entity id and name provided by action source if available.

State Controller

Reference to Dashboard state controller (IStateController), providing API to manage current dashboard state.

Function Description
openState(id, params, openRightLayout) Navigates to new dashboard state. id - id of the target dashboard state, params - object with state parameters to use by the new state, openRightLayout - optional boolean argument to force open right dashboard layout if present in mobile view mode.
updateState(id, params, openRightLayout) Updates current dashboard state. id - optional id of the target dashboard state to replace current state id, params - object with state parameters to update current state parameters, openRightLayout - optional boolean argument to force open right dashboard layout if present in mobile view mode.
getStateId() Returns current dashboard state id.
getStateParams() Returns current dashboard state parameters.
getStateParamsByStateId(id) Returns state parameters for particular dashboard state identified by id.

Type parameters object

Object WidgetTypeParameters describing widget datasource parameters. It has the following properties:

  1. return {
  2. maxDatasources: -1, // Maximum allowed datasources for this widget, -1 - unlimited
  3. maxDataKeys: -1, //Maximum allowed data keys for this widget, -1 - unlimited
  4. dataKeysOptional: false //Whether this widget can be configured with datasources without data keys
  5. }

Action sources object

Map describing available widget action sources (WidgetActionSource) to which user actions can be assigned. It has the following structure:

  1. return {
  2. 'headerButton': { // Action source Id (unique action source identificator)
  3. name: 'Header button', // Display name of action source, used in widget settings ('Actions' tab).
  4. multiple: true // Boolean property indicating if this action source supports multiple action definitions (for ex. multiple buttons in one cell, or only one action can by assigned on table row click.)
  5. }
  6. };

Creating simple widgets

The tutorials below show how to create minimal widgets of each type. In order to minimize the amount of code, the Angular framework will be used, on which ThingsBoard UI is actually based. By the way, you can always use pure JavaScript or jQuery API in your widget code.

Latest Values widget

In the Widgets Bundle view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button. Click the Latest Values button on the Select widget type popup. The Widget Editor will open, pre-populated with the content of the default Latest Values template widget.

  • Clear content of the CSS tab of “Resources” section.
  • Put the following HTML code inside the HTML tab of “Resources” section:

    1. <div fxFlex fxLayout="column" style="height: 100%;" fxLayoutAlign="center stretch">
    2. <div>My first latest values widget.</div>
    3. <div fxFlex fxLayout="row" *ngFor="let dataKeyData of data" fxLayoutAlign="space-around center">
    4. <div>{{dataKeyData.dataKey.label}}:</div>
    5. <div>{{(dataKeyData.data[0] && dataKeyData.data[0][0]) | date : 'yyyy-MM-dd HH:mm:ss' }}</div>
    6. <div>{{dataKeyData.data[0] && dataKeyData.data[0][1]}}</div>
    7. </div>
    8. </div>
  • Put the following JavaScript code inside the “JavaScript” section:

    1. self.onInit = function() {
    2. self.ctx.$scope.data = self.ctx.defaultSubscription.data;
    3. }
    4. self.onDataUpdated = function() {
    5. self.ctx.detectChanges();
    6. }
  • Click the Run button on the Widget Editor Toolbar in order to see the result in Widget preview section.

部件Widgets3.x开发 - 图12
In this example, the data property of subscription is assigned to the $scope and becomes accessible within the HTML template. Inside the HTML, a special *ngFor structural angular directive is used in order to iterate over available dataKeys & datapoints then render latest values with their corresponding timestamps.

Time-Series widget

In the Widgets Bundle view, click the big “+” button at the bottom-right part of the screen, then click the “Create new widget type” button. Click the Time-Series button on the Select widget type popup. The Widget Editor will open, pre-populated with default Time-Series template widget content.

  • Replace content of the CSS tab in “Resources” section with the following one:

    1. .my-data-table th {
    2. text-align: left;
    3. }
  • Put the following HTML code inside the HTML tab of “Resources” section:

    1. <mat-tab-group style="height: 100%;">
    2. <mat-tab *ngFor="let datasource of datasources; let $dsIndex = index" label="{{datasource.name}}">
    3. <table class="my-data-table" style="width: 100%;">
    4. <thead>
    5. <tr>
    6. <th>Timestamp</th>
    7. <th *ngFor="let dataKeyData of datasourceData[$dsIndex]">{{dataKeyData.dataKey.label}}</th>
    8. <tr>
    9. </thead>
    10. <tbody>
    11. <tr *ngFor="let data of datasourceData[$dsIndex][0].data; let $dataIndex = index">
    12. <td>{{data[0] | date : 'yyyy-MM-dd HH:mm:ss'}}</td>
    13. <td *ngFor="let dataKeyData of datasourceData[$dsIndex]">{{dataKeyData.data[$dataIndex] && dataKeyData.data[$dataIndex][1]}}</td>
    14. </tr>
    15. </tbody>
    16. </table>
    17. </mat-tab>
    18. </mat-tab-group>
  • Put the following JavaScript code inside the “JavaScript” section:

    1. self.onInit = function() {
    2. self.ctx.widgetTitle = 'My first Time-Series widget';
    3. self.ctx.$scope.datasources = self.ctx.defaultSubscription.datasources;
    4. self.ctx.$scope.data = self.ctx.defaultSubscription.data;
    5. self.ctx.$scope.datasourceData = [];
    6. var currentDatasource = null;
    7. var currentDatasourceIndex = -1;
    8. for (var i=0;i<self.ctx.$scope.data.length;i++) {
    9. var dataKeyData = self.ctx.$scope.data[i];
    10. if (dataKeyData.datasource != currentDatasource) {
    11. currentDatasource = dataKeyData.datasource
    12. currentDatasourceIndex++;
    13. self.ctx.$scope.datasourceData[currentDatasourceIndex] = [];
    14. }
    15. self.ctx.$scope.datasourceData[currentDatasourceIndex].push(dataKeyData);
    16. }
    17. self.ctx.updateWidgetParams();
    18. }
    19. self.onDataUpdated = function() {
    20. self.ctx.detectChanges();
    21. }
  • Click the Run button on the Widget Editor Toolbar in order to see the result in Widget preview section.

部件Widgets3.x开发 - 图13
In this example, the subscription datasources and data properties are assigned to $scope and become accessible within the HTML template. The $scope.datasourceData property is introduced to map datasource specific dataKeys data by datasource index for flexible access within the HTML template. Inside the HTML, a special *ngFor structural angular directive is used in order to iterate over available datasources and render corresponding tabs. Inside each tab, the table is rendered using dataKeys obtained from datasourceData scope property accessed by datasource index. Each table renders columns by iterating over all dataKeyData objects and renders all available datapoints by iterating over data array of each dataKeyData to render timestamps and values. Note that in this code, onDataUpdated function is implemented with a call to detectChanges function necessary to perform new change detection cycle when new data is received.

RPC (Control) widget

In the Widgets Bundle view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button. Click the Control Widget button on the Select widget type popup. The Widget Editor will open, pre-populated with default Control template widget content.

  • Clear content of the CSS tab of “Resources” section.
  • Put the following HTML code inside the HTML tab of “Resources” section:

    1. <form #rpcForm="ngForm" (submit)="sendCommand()">
    2. <div class="mat-content mat-padding" fxLayout="column">
    3. <mat-form-field class="mat-block">
    4. <mat-label>RPC method</mat-label>
    5. <input matInput required name="rpcMethod" #rpcMethodField="ngModel" [(ngModel)]="rpcMethod"/>
    6. <mat-error *ngIf="rpcMethodField.hasError('required')">
    7. RPC method name is required.
    8. </mat-error>
    9. </mat-form-field>
    10. <mat-form-field class="mat-block">
    11. <mat-label>RPC params</mat-label>
    12. <input matInput required name="rpcParams" #rpcParamsField="ngModel" [(ngModel)]="rpcParams"/>
    13. <mat-error *ngIf="rpcParamsField.hasError('required')">
    14. RPC params is required.
    15. </mat-error>
    16. </mat-form-field>
    17. <button [disabled]="rpcForm.invalid || !rpcForm.dirty" mat-raised-button color="primary" type="submit" >
    18. Send RPC command
    19. </button>
    20. <div>
    21. <label>RPC command response</label>
    22. <div style="width: 100%; height: 100px; border: solid 2px gray" [innerHTML]="rpcCommandResponse">
    23. </div>
    24. </div>
    25. </div>
    26. </form>
  • Put the following JSON content inside the “Settings schema” tab of Settings schema section:

    1. {
    2. "schema": {
    3. "type": "object",
    4. "title": "Settings",
    5. "properties": {
    6. "oneWayElseTwoWay": {
    7. "title": "Is One Way Command",
    8. "type": "boolean",
    9. "default": true
    10. },
    11. "requestTimeout": {
    12. "title": "RPC request timeout",
    13. "type": "number",
    14. "default": 500
    15. }
    16. },
    17. "required": []
    18. },
    19. "form": [
    20. "oneWayElseTwoWay",
    21. "requestTimeout"
    22. ]
    23. }
  • Put the following JavaScript code inside the “JavaScript” section: ``` self.onInit = function() {

    self.ctx.$scope.sendCommand = function() {

    1. var rpcMethod = self.ctx.$scope.rpcMethod;
    2. var rpcParams = self.ctx.$scope.rpcParams;
    3. var timeout = self.ctx.settings.requestTimeout;
    4. var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ? true : false;
    5. var commandObservable;
    6. if (oneWayElseTwoWay) {
    7. commandObservable = self.ctx.controlApi.sendOneWayCommand(rpcMethod, rpcParams, timeout);
    8. } else {
    9. commandObservable = self.ctx.controlApi.sendTwoWayCommand(rpcMethod, rpcParams, timeout);
    10. }
    11. commandObservable.subscribe(
    12. function (response) {
    13. if (oneWayElseTwoWay) {
    14. self.ctx.$scope.rpcCommandResponse = "Command was successfully received by device.<br> No response body because of one way command mode.";
    15. } else {
    16. self.ctx.$scope.rpcCommandResponse = "Response from device:<br>";
    17. self.ctx.$scope.rpcCommandResponse += JSON.stringify(response, undefined, 2);
    18. }
    19. self.ctx.detectChanges();
    20. },
    21. function (rejection) {
    22. self.ctx.$scope.rpcCommandResponse = "Failed to send command to the device:<br>"
    23. self.ctx.$scope.rpcCommandResponse += "Status: " + rejection.status + "<br>";
    24. self.ctx.$scope.rpcCommandResponse += "Status text: '" + rejection.statusText + "'";
    25. self.ctx.detectChanges();
    26. }
    27. );

    }

}

  1. - Fill **Widget title** field with widget type name, for ex. My first control widget”.
  2. - Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
  3. - Click dashboard edit button on the preview section to change the size of the resulting widget. Then click dashboard apply button. The final widget should look like the image below.
  4. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778373-ccfffb2c-f1b8-4afb-9711-64f652935306.png#align=left&display=inline&height=413&margin=%5Bobject%20Object%5D&originHeight=413&originWidth=834&size=0&status=done&style=none&width=834)
  5. - Click the **Save** button on the **Widget Editor Toolbar** to save widget type.
  6. To test how this widget performs RPC commands, we will need to place it in a dashboard then bind it to a device working with RPC commands. To do this, perform the following steps:
  7. - Login as Tenant administrator.
  8. - Navigate to **Devices** and create new device with some name, for ex. My RPC Device”.
  9. - Open device details and click Copy Access Token button to copy device access token to clipboard.
  10. - Download [mqtt-js-rpc-from-server.sh](https://thingsboard.io/docs/reference/resources/mqtt-js-rpc-from-server.sh) and [mqtt-js-rpc-from-server.js](https://thingsboard.io/docs/reference/resources/mqtt-js-rpc-from-server.js). Place these files in a folder. Edit **mqtt-js-rpc-from-server.sh** - replace **$ACCESS_TOKEN** with your device access token from the clipboard.
  11. - Run **mqtt-js-rpc-from-server.sh** script. You should see a connected message in the console.
  12. - Navigate to **Dashboards** and create a new dashboard with some name, for ex. My first control dashboard”. Open this dashboard.
  13. - Click dashboard edit button. In the dashboard edit mode, click the Entity aliases button located on the dashboard toolbar.
  14. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778443-c43dae77-510e-4a91-8451-fc24d9a7a321.png#align=left&display=inline&height=107&margin=%5Bobject%20Object%5D&originHeight=107&originWidth=431&size=0&status=done&style=none&width=431)
  15. - Inside **Entity aliases** popup click Add alias”.
  16. - Fill Alias name field, for ex. My RPC Device Alias”.
  17. - Select Entity list in Filter type field.
  18. - Choose Device in Type field.
  19. - Select your device in Entity list field. In this example My RPC Device”.
  20. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778490-699ec692-5405-414d-a914-23e35abedf4b.png#align=left&display=inline&height=474&margin=%5Bobject%20Object%5D&originHeight=474&originWidth=595&size=0&status=done&style=none&width=595)
  21. - Click Add and then Save in **Entity aliases**.
  22. - Click dashboard “+” button then click Create new widget button.
  23. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778428-56f4387b-878b-48e3-b50c-16060d9c3133.png#align=left&display=inline&height=214&margin=%5Bobject%20Object%5D&originHeight=214&originWidth=231&size=0&status=done&style=none&width=231)
  24. - Then select **Widget Bundle** where your RPC widget was saved. Select Control widget tab.
  25. - Click your widget. In this example, My first control widget”.
  26. - From **Add Widget** popup, select your device alias in **Target device** section. In this example My RPC Device Alias”.
  27. - Click **Add**. Your Control widget will appear in the dashboard. Click dashboard **Apply changes** button to save dashboard and leave editing mode.
  28. - Fill **RPC method** field with RPC method name. For ex. TestMethod”.
  29. - Fill **RPC params** field with RPC params. For ex. “{ param1: value1 }”.
  30. - Click **Send RPC command** button. You should see the following response in the widget.
  31. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778419-618e18f1-b178-4fe4-8365-56ebdc39a0bf.png#align=left&display=inline&height=210&margin=%5Bobject%20Object%5D&originHeight=210&originWidth=498&size=0&status=done&style=none&width=498)<br />The following output should be printed in the device console:

request.topic: v1/devices/me/rpc/request/0 request.body: {“method”:”TestMethod”,”params”:”{ param1: \”value1\” }”}

  1. In order to test Two way RPC command mode, we need to change the corresponding widget settings property. To do this, perform the following steps:
  2. - Click dashboard edit button. In dashboard edit mode, click **Edit widget** button located in the header of Control widget.
  3. - In the widget details, view select Advanced tab and uncheck Is One Way Command checkbox.
  4. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778371-ff957bbf-96c8-4e32-a638-906b4e3e2787.png#align=left&display=inline&height=192&margin=%5Bobject%20Object%5D&originHeight=192&originWidth=705&size=0&status=done&style=none&width=705)
  5. - Click **Apply changes** button on the widget details header. Close details and click dashboard **Apply changes** button.
  6. - Fill widget fields with RPC method name and params like in previous steps. Click **Send RPC command** button. You should see the following response in the widget.
  7. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778392-4a41b055-be3a-418c-9f7e-84fc8ede9921.png#align=left&display=inline&height=177&margin=%5Bobject%20Object%5D&originHeight=177&originWidth=488&size=0&status=done&style=none&width=488)
  8. - stop **mqtt-js-rpc-from-server.sh** script. Click **Send RPC command** button. You should see the following response in the widget.
  9. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765779389-be168be0-d763-4a78-aff5-80566da7da80.png#align=left&display=inline&height=177&margin=%5Bobject%20Object%5D&originHeight=177&originWidth=486&size=0&status=done&style=none&width=486)<br />In this example, **controlApi** is used to send RPC commands. Additionally, custom widget settings were introduced in order to configure RPC command mode and RPC request timeout. The response from the device is handled by **commandObservable**. It has success and failed callbacks with corresponding response, or rejection objects containing information about request execution result.
  10. <a name="alarm-widget"></a>
  11. #### Alarm widget
  12. In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the Create new widget type button. Click the **Alarm Widget** button on the **Select widget type** popup. The **Widget Editor** will be opened, pre-populated with the content of the default **Alarm** template widget.
  13. - Replace content of the CSS tab in Resources section with the following one:

.my-alarm-table th { text-align: left; }

  1. - Put the following HTML code inside the HTML tab of Resources section:

My first Alarm widget.



{{dataKey.label}}
{{getAlarmValue(alarm, dataKey)}}

  1. - Put the following JSON content inside the Settings schema tab of **Settings schema section**:

{ “schema”: { “type”: “object”, “title”: “AlarmTableSettings”, “properties”: { “alarmSeverityColorFunction”: { “title”: “Alarm severity color function: f(severity)”, “type”: “string”, “default”: “if(severity == ‘CRITICAL’) {return ‘red’;} else if (severity == ‘MAJOR’) {return ‘orange’;} else return ‘green’; “ } }, “required”: [] }, “form”: [ { “key”: “alarmSeverityColorFunction”, “type”: “javascript” } ] }

  1. - Put the following JavaScript code inside the JavaScript section:

self.onInit = function() { self.ctx.$scope.alarmSource = self.ctx.defaultSubscription.alarmSource;

  1. var alarmSeverityColorFunctionBody = self.ctx.settings.alarmSeverityColorFunction;
  2. if (typeof alarmSeverityColorFunctionBody === 'undefined' || !alarmSeverityColorFunctionBody.length) {
  3. alarmSeverityColorFunctionBody = "if(severity == 'CRITICAL') {return 'red';} else if (severity == 'MAJOR') {return 'orange';} else return 'green';";
  4. }
  5. var alarmSeverityColorFunction = null;
  6. try {
  7. alarmSeverityColorFunction = new Function('severity', alarmSeverityColorFunctionBody);
  8. } catch (e) {
  9. alarmSeverityColorFunction = null;
  10. }
  11. self.ctx.$scope.getAlarmValue = function(alarm, dataKey) {
  12. var alarmKey = dataKey.name;
  13. if (alarmKey === 'originator') {
  14. alarmKey = 'originatorName';
  15. }
  16. var value = alarm[alarmKey];
  17. if (alarmKey === 'createdTime') {
  18. return self.ctx.date.transform(value, 'yyyy-MM-dd HH:mm:ss');
  19. } else {
  20. return value;
  21. }
  22. }
  23. self.ctx.$scope.getAlarmCellStyle = function(alarm, dataKey) {
  24. var alarmKey = dataKey.name;
  25. if (alarmKey === 'severity' && alarmSeverityColorFunction) {
  26. var severity = alarm[alarmKey];
  27. var color = alarmSeverityColorFunction(severity);
  28. return {
  29. color: color
  30. };
  31. }
  32. return {};
  33. }

} self.onDataUpdated = function() { self.ctx.$scope.alarms = self.ctx.defaultSubscription.alarms; self.ctx.detectChanges(); }

  1. - Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
  2. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778501-6ac210c2-8445-45a5-93ec-fda5fdbe01bb.png#align=left&display=inline&height=476&margin=%5Bobject%20Object%5D&originHeight=476&originWidth=826&size=0&status=done&style=none&width=826)<br />In this example, the **alarmSource** and **alarms** properties of [subscription](https://thingsboard.io/docs/user-guide/contribution/widgets-development/#subscription-object) are assigned to **$scope** and become accessible within HTML template. Inside the HTML, a special [***ngFor**](https://angular.io/api/common/NgForOf) structural angular directive is used in order to iterate over available alarm **dataKeys** of **alarmSource** and render corresponding columns. The table rows are rendered by iterating over **alarms** array and corresponding cells rendered by iterating over **dataKeys**. The function **getAlarmValue** is fetching alarm value and formatting **createdTime** alarm property using a [DatePipe](https://angular.io/api/common/DatePipe) angular pipe accessible via **date** property of **ctx**. The function **getAlarmCellStyle** is used to assign custom cell styles for each alarm cell. In this example, we introduced new settings property called **alarmSeverityColorFunction** that contains function body returning color depending on alarm severity. Inside the **getAlarmCellStyle** function there is corresponding invocation of **alarmSeverityColorFunction** with severity value in order to get color for alarm severity cell. Note that in this code **onDataUpdated** function is implemented in order to update **alarms** property with latest alarms from subscription and invoke change detection using **detectChanges()** function.
  3. <a name="static-widget"></a>
  4. #### Static widget
  5. In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the Create new widget type button. Click the **Static Widget** button on the **Select widget type** popup. The **Widget Editor** will be opened pre-populated with the content of default **Static** template widget.
  6. - Put the following HTML code inside the HTML tab of Resources section:

My first static widget.

  1. - Put the following JSON content inside the Settings schema tab of **Settings schema section**:

{ “schema”: { “type”: “object”, “title”: “Settings”, “properties”: { “alertContent”: { “title”: “Alert content”, “type”: “string”, “default”: “Content derived from alertContent property of widget settings.” } } }, “form”: [ “alertContent” ] }

  1. - Put the following JavaScript code inside the JavaScript section:

self.onInit = function() { self.ctx.$scope.showAlert = function() { var alertContent = self.ctx.settings.alertContent; if (!alertContent) { alertContent = “Content derived from alertContent property of widget settings.”; } window.alert(alertContent);
}; }

  1. - Click the **Run** button on the **Widget Editor Toolbar** to see the resulting **Widget preview** section.
  2. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778383-ad9c0de0-a06c-4a4a-94b3-34903aa77530.png#align=left&display=inline&height=320&margin=%5Bobject%20Object%5D&originHeight=320&originWidth=823&size=0&status=done&style=none&width=823)<br />This is just a static HTML widget. There is no subscription data and no special widget API was used. Only custom **showAlert** function was implemented showing an alert with the content of **alertContent** property of widget settings. You can switch to dashboard edit mode in **Widget preview** section and change value of **alertContent** by changing widget settings in the “Advanced” tab of widget details. Then you can see that the new alert content is displayed.
  3. <a name="integrating-existing-code-to-create-widget-definition"></a>
  4. ## Integrating existing code to create widget definition
  5. Below are some examples demonstrating how external JavaScript libraries or existing code can be reused/integrated to create new widgets.
  6. <a name="using-external-javascript-library"></a>
  7. ### Using external JavaScript library
  8. <a name="latest-values-example"></a>
  9. #### Latest Values Example
  10. In this example, **Latest Values** gauge widget will be created using external [gauge.js](http://bernii.github.io/gauge.js/) library.<br />In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen, then click the “Create new widget type” button. Click the **Latest Values** button on the **Select widget type** popup. The **Widget Editor** will be opened, pre-populated with the content of default **Latest Values** template widget.
  11. - Open **Resources** tab and click Add then insert the following link:

https://bernii.github.io/gauge.js/dist/gauge.min.js

  1. - Clear content of the CSS tab of Resources section.
  2. - Put the following HTML code inside the HTML tab of Resources section:

  1. - Put the following JavaScript code inside the JavaScript section:

var canvasElement; var gauge; self.onInit = function() { canvasElement = $(‘#my-gauge’, self.ctx.$container)[0]; gauge = new Gauge(canvasElement); gauge.minValue = -1000; gauge.maxValue = 1000; gauge.animationSpeed = 16; self.onResize(); } self.onResize = function() { canvasElement.width = self.ctx.width; canvasElement.height = self.ctx.height; gauge.update(true); gauge.render(); } self.onDataUpdated = function() { var value = self.ctx.defaultSubscription.data[0].data[0][1]; gauge.set(value); }

  1. - Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
  2. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778439-bea94d67-93f0-4867-97f3-d58a8abe2850.png#align=left&display=inline&height=310&margin=%5Bobject%20Object%5D&originHeight=310&originWidth=823&size=0&status=done&style=none&width=823)<br />In this example, the external JS library API was used that becomes available after injecting the corresponding URL in **Resources** section. The value displayed was obtained from [subscription](https://thingsboard.io/docs/user-guide/contribution/widgets-development/#subscription-object) **data** property for the first dataKey.
  3. <a name="time-series-example"></a>
  4. #### Time-Series Example
  5. In this example, **Time-Series** line chart widget will be created using external [Chart.js](https://www.chartjs.org/) library.<br />In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen, then click the “Create new widget type” button. Click the **Time-Series** button on the **Select widget type** popup. The **Widget Editor** will be opened, pre-populated with the content of default **Time-Series** template widget.
  6. - Open **Resources** tab and click Add then insert the following link:

https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js

  1. - Clear content of the CSS tab of Resources section.
  2. - Put the following HTML code inside the HTML tab of Resources section:

  1. - Put the following JavaScript code inside the JavaScript section:

var myChart; self.onInit = function() {

  1. var chartData = {
  2. datasets: []
  3. };
  4. for (var i=0; i < self.ctx.data.length; i++) {
  5. var dataKey = self.ctx.data[i].dataKey;
  6. var dataset = {
  7. label: dataKey.label,
  8. data: [],
  9. borderColor: dataKey.color,
  10. fill: false
  11. };
  12. chartData.datasets.push(dataset);
  13. }
  14. var options = {
  15. maintainAspectRatio: false,
  16. legend: {
  17. display: false
  18. },
  19. scales: {
  20. xAxes: [{
  21. type: 'time',
  22. ticks: {
  23. maxRotation: 0,
  24. autoSkipPadding: 30
  25. }
  26. }]
  27. }
  28. };
  29. var canvasElement = $('#myChart', self.ctx.$container)[0];
  30. var canvasCtx = canvasElement.getContext('2d');
  31. myChart = new Chart(canvasCtx, {
  32. type: 'line',
  33. data: chartData,
  34. options: options
  35. });
  36. self.onResize();

} self.onResize = function() { myChart.resize(); } self.onDataUpdated = function() { for (var i = 0; i < self.ctx.data.length; i++) { var datasourceData = self.ctx.data[i]; var dataSet = datasourceData.data; myChart.data.datasets[i].data.length = 0; var data = myChart.data.datasets[i].data; for (var d = 0; d < dataSet.length; d++) { var tsValuePair = dataSet[d]; var ts = tsValuePair[0]; var value = tsValuePair[1]; data.push({t: ts, y: value}); } } myChart.options.scales.xAxes[0].ticks.min = self.ctx.timeWindow.minTime; myChart.options.scales.xAxes[0].ticks.max = self.ctx.timeWindow.maxTime; myChart.update(); }

  1. - Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
  2. ![](https://cdn.nlark.com/yuque/0/2021/png/667839/1610765778643-32b8aec3-04e7-4aee-aa26-9e1b821cf11c.png#align=left&display=inline&height=520&margin=%5Bobject%20Object%5D&originHeight=520&originWidth=822&size=0&status=done&style=none&width=822)<br />In this example, the external JS library API was used that becomes available after injecting the corresponding URL in **Resources** section. Initially chart datasets prepared using configured dataKeys from **data** property of **ctx**. In the **onDataUpdated** function datasources data converted to Chart.js line chart format and pushed to chart datasets. Please note that xAxis (time axis) is limited to current timewindow bounds obtained from **timeWindow** property of **ctx**.
  3. <a name="using-existing-javascript-code"></a>
  4. ### Using existing JavaScript code
  5. Another approach of creating widgets is to use existing bundled JavaScript code. In this case, you can create own TypeScript class or Angular component and bundle it into the ThingsBoard UI code. In order to make this code accessible within the widget, you need to register corresponding Angular module or inject TypeScript class to a global variable (for ex. window object). Some of the ThingsBoard widgets already use this approach. Take a look at the [polyfills.ts](https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/polyfills.ts#L106) or [widget-components.module.ts](https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts#L44). Here you can find how some bundled classes or components are registered for later use in ThingsBoard widgets. For example “Timeseries - Flot” widget (from “Charts” Widgets Bundle) uses [**TbFlot**](https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts#L63) TypeScript class which is injected as window property inside **polyfills.ts**:

… import { TbFlot } from ‘@home/components/widget/lib/flot-widget’; … (window as any).TbFlot = TbFlot; …

  1. Another example is Timeseries table widget (from Cards Widgets Bundle) that uses Angular component [**tb-timeseries-table-widget**](https://github.com/thingsboard/thingsboard/blob/13e6b10b7ab830e64d31b99614a9d95a1a25928a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L99) which is registered as dependency of **WidgetComponentsModule** Angular module inside **widget-components.module.ts**. Thereby this component becomes available for use inside the widget template HTML.

… import { TimeseriesTableWidgetComponent } from ‘@home/components/widget/lib/timeseries-table-widget.component’; … @NgModule({ declarations: [ … TimeseriesTableWidgetComponent, … ], … exports: [ … TimeseriesTableWidgetComponent, … ], …
}) export class WidgetComponentsModule { } ```

Widget code debugging tips

The most simple method of debugging is Web console output. Just place console.log(…) function inside any part of widget JavaScript code. Then click Run button to restart widget code and observe debug information in the Web console.
Another and most effective method of debugging is to invoke browser debugger. Put debugger; statement into the place of widget code you are interested in and then click Run button to restart widget code. Browser debugger (if enabled) will automatically pause code execution at the debugger statement and you will be able to analyze script execution using browser debugging tools.

Next steps