ThingsBoard / フィルター付きDashboardの作成
Overview
複数のDeviceに共通なDashboardを作るケースは多々あります。同じプロダクトの2つの装置(Device1とDevice2)がある場合に、ひとつだけDashboardを作っておいてDevice1とDevice2を切り替えてデータを見られるようにする場合などです。
当然のことながらThingsBoardのDashboardにはフィルター機能が標準であるので、これを使えば指定したDeviceのデータのみを表示することはできます。標準機能ではありますが、これを使いこなすまで一苦労しました。
さらに。このフィルター機能では、フィルターするのにテキスト入力するしかありません。
テキスト入力すればいいので大きな問題はないのですが、Deviceでフィルターする場合など決まった値しかない場合には選択式で選ぶことができた方がUI/UXとしてはベターです。
本記事ではフィルター機能の使い方から、選択式でフィルターを適用する方法について解説します。選択式のフィルターに関してはJavaScript書く必要があったりとAdvancedな内容になりますが、この手法を知ることでDashboardの作り方の幅が広がると思います。
作るもの
Deviceの一覧を表示し、フィルターしたいDeviceをクリック/タップすると、そのDeviceの値が表示されるDashboardを作ります。こんなイメージです。device1には Temperature: 42
が、 device2には Temperature: 100
が設定されていて、下の折れ線グラフが切り替わっています。(動画がもっさりしてますが、数秒我慢して見つめていてください)
手順
Deviceを用意する
2つのDeviceを用意します。名前は適当でかまいませんがここではdevice1、device2としています。Device Profile/Rule Chainも、Telemetryが問題なく反映される限りにおいて適当なものでかまいませんが、後の設定のため2つのDeviceに同じDevice Profileを割り当てておきます。
Rule Chainを作る
用意したDeviceに対して、定期的にTelemetryをPostするように設定しておきます。どんなやり方でもかまいませんが、ここではRule Chainを使ってダミーデータをPostさせてみます。
generatorノードは、その名の通りデータを生成するノードです。さきほど作成した2つのDeviceそれぞれに対してデータを生成するために2つ作成しています。一方のノードでは Generated messages limit
だけ無限のデータを生成しないように 1000
にしていますが、それ以外は初期値のままです。もう一方には、同じくGenerated messages limit
の変更と、完成時のDeviceの切り替えがわかりやすいように temp
の値を42から適当な数字に変更します。
change originatorノードでは、generatorノードがそれぞれdevice1, device2のTelemetryであると設定するために入れています。画像はdevice1のものですが、device2では Name pattern
を device2
にしています。
save time series
ノードで、各DeviceにTelemetryを登録するようにしています。特に設定は必要ありません。
ここまで設定してRule Chainを保存すれば、Generatorが定期的に動作し、各DeviceのLatest telemetryに値が反映されます。
以下はこのRule ChainのJSONです。importすればそのまま使えます。
Rule Chain JSON
{
"ruleChain": {
"name": "Telemetry Generator",
"type": "CORE",
"firstRuleNodeId": null,
"root": false,
"debugMode": false,
"configuration": null,
"additionalInfo": {
"description": ""
}
},
"metadata": {
"version": 7,
"firstNodeIndex": null,
"nodes": [
{
"type": "org.thingsboard.rule.engine.debug.TbMsgGeneratorNode",
"name": "Device1",
"debugSettings": {
"failuresEnabled": false,
"allEnabled": false,
"allEnabledUntil": 1739408696359
},
"singletonMode": false,
"queueName": null,
"configurationVersion": 2,
"configuration": {
"msgCount": 1000,
"periodInSeconds": 60,
"scriptLang": "TBEL",
"jsScript": "var msg = { temp: 42, humidity: 77 };\nvar metadata = { data: 40 };\nvar msgType = "POST_TELEMETRY_REQUEST";\n\nreturn { msg: msg, metadata: metadata, msgType: msgType };",
"tbelScript": "var msg = { temp: 42, humidity: 77 };\nvar metadata = { data: 40 };\nvar msgType = "POST_TELEMETRY_REQUEST";\n\nreturn { msg: msg, metadata: metadata, msgType: msgType };",
"originatorId": null,
"originatorType": "RULE_NODE"
},
"additionalInfo": {
"description": "",
"layoutX": 92,
"layoutY": 256
}
},
{
"type": "org.thingsboard.rule.engine.transform.TbChangeOriginatorNode",
"name": "device1",
"debugSettings": {
"failuresEnabled": false,
"allEnabled": false,
"allEnabledUntil": 1739408696359
},
"singletonMode": false,
"queueName": null,
"configurationVersion": 1,
"configuration": {
"originatorSource": "ENTITY",
"preserveOriginatorIfCustomer": false,
"entityType": "DEVICE",
"entityNamePattern": "device1",
"relationsQuery": {
"direction": "FROM",
"maxLevel": 1,
"filters": [
{
"relationType": "Contains",
"entityTypes": [],
"negate": false
}
],
"fetchLastLevelOnly": false
}
},
"additionalInfo": {
"description": "",
"layoutX": 408,
"layoutY": 261
}
},
{
"type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
"name": "Save Timeseries",
"debugSettings": {
"failuresEnabled": false,
"allEnabled": false,
"allEnabledUntil": 1739408832424
},
"singletonMode": false,
"queueName": null,
"configurationVersion": 0,
"configuration": {
"defaultTTL": 0,
"skipLatestPersistence": false,
"useServerTs": false
},
"additionalInfo": {
"description": "",
"layoutX": 736,
"layoutY": 260
}
},
{
"type": "org.thingsboard.rule.engine.transform.TbChangeOriginatorNode",
"name": "device2",
"debugSettings": {
"failuresEnabled": false,
"allEnabled": false,
"allEnabledUntil": 1739408972214
},
"singletonMode": false,
"queueName": null,
"configurationVersion": 1,
"configuration": {
"originatorSource": "ENTITY",
"preserveOriginatorIfCustomer": false,
"entityType": "DEVICE",
"entityNamePattern": "device2",
"relationsQuery": {
"direction": "FROM",
"maxLevel": 1,
"filters": [
{
"relationType": "Contains",
"entityTypes": [],
"negate": false
}
],
"fetchLastLevelOnly": false
}
},
"additionalInfo": {
"description": "",
"layoutX": 407,
"layoutY": 343
}
},
{
"type": "org.thingsboard.rule.engine.debug.TbMsgGeneratorNode",
"name": "Device2",
"debugSettings": {
"failuresEnabled": false,
"allEnabled": false,
"allEnabledUntil": 1739408972214
},
"singletonMode": false,
"queueName": null,
"configurationVersion": 2,
"configuration": {
"msgCount": 1000,
"periodInSeconds": 60,
"scriptLang": "TBEL",
"jsScript": "var msg = { temp: 42, humidity: 77 };\nvar metadata = { data: 40 };\nvar msgType = "POST_TELEMETRY_REQUEST";\n\nreturn { msg: msg, metadata: metadata, msgType: msgType };",
"tbelScript": "var msg = { temp: 100, humidity: 77 };\nvar metadata = { data: 40 };\nvar msgType = "POST_TELEMETRY_REQUEST";\n\nreturn { msg: msg, metadata: metadata, msgType: msgType };",
"originatorId": null,
"originatorType": "RULE_NODE"
},
"additionalInfo": {
"description": "",
"layoutX": 88,
"layoutY": 344
}
}
],
"connections": [
{
"fromIndex": 0,
"toIndex": 1,
"type": "Success"
},
{
"fromIndex": 1,
"toIndex": 2,
"type": "Success"
},
{
"fromIndex": 3,
"toIndex": 2,
"type": "Success"
},
{
"fromIndex": 4,
"toIndex": 3,
"type": "Success"
}
],
"ruleChainConnections": null
}
}
Dashboard以外の準備は完了です。Dashboardの作成に入ります。
ベースとなるDashboardを作成する
まずは、さきほど作ったdevice1とdevice2のデータが両方表示されるようにDashboardを作ります。
Aliasの作成
Dashboardの編集画面でAliasを作ります。以下のように設定します。さきほど作成したDeviceには Device Profile default
が割り当ててあるのでこれを指定します。
Line Chart Widgetの作成
Line Chart Widgetを作成して、2つのDeviceの temp
フィールドの値を表示させます。 Advanced
モードに切り替えます(この時点でAdvancedモードである必要はまだありませんが)。 Type
に Entity
を、 Entity alias
にさきほど作ったAliasを、Time series data keys
に temp
を指定します。
Widgetを保存すれば以下のようなDashboardができあがります。
これでベースとなるDashboardは完成です。
テキスト入力でフィルターできるようにする
続いてテキスト入力でdevice1 or device2のいずれかのみを表示できるようにします。
まず、下図の導線からFilter編集画面を開きます。
Key Type
に Entity field
、Key name
に name
、Value type
に String
を選択します。Filters
ブロックでは、Operation
にequal
、Value
にdevice1
を指定します。
その後保存します。
名称(name
)が device1
であるEntity(ここでは Device
を想定)のみに絞るフィルターをここで作っています。
この時点ではDashboard内のすべてのWidgetで利用できるフィルター要素を作っただけで、Chartにはフィルターが適用されていません。適用させます。
再度、Line chart widgetの設定を開きます。Filter
にさきほど作ったFilter(ここでは DeviceName
)を選択して保存します。
これで折れ線グラフ上にdevice1のみが表示されるようになりました。
ダッシュボードを保存して、Filterの値(device1 or device2)を入力すると、その値に従ってLine Chartに表示される内容が切り替わります。
これでやっとテキスト入力によるフィルターの適用ができました。
選択式でフィルター条件を変更できるようにする
先述した通り、テキスト入力だと不便です。選択式でフィルターを適用できるように手を加えます。
複雑なので何をやろうとしている図解します。ここまで作成したDashboardでは「テキスト入力」によりFilterの値を変更すると、Filterに紐づけたLine Chart Widgetにもフィルターが適用されるようになっています。
選択式フィルターでは、Deviceの一覧を表示するEntities Table Widgetを準備し、これに表示されるDeviceをClick/TapすることでいったんUser Attributeとしてその値を保持します。FilterはこのUser Attributeと紐づいており、User Attributeが変更されればフィルターの値も変わる、結果Line Chart Widgetのフィルターも更新される、という仕組みで実現します。
Deviceの一覧を作成
Deviceの一覧を表示するWidgetを作成して、その一覧上のDeviceをクリックしたらその値でフィルターを更新する形でDashboardを作ります。
Add widgetからEntities tableを選択、DeviceからEntity aliasに切り替え、Entity aliasにさきほど作ったDeviceのAliasを指定します。
保存後、Deviceの一覧(device1とdevice2)が表示されます。
Actionを実装する
Device一覧のWidgetにActionを実装して、DeviceをClick/TapしたらUser Attributeが更新されるようにします。
今作成したDevice一覧のWidgetの設定画面を開き、一番下にある"Actions"パートの"Add action"を選択します。
Actionの設定画面が開きますので、「+」ボタンで進みます。
以下図のように設定します。
- Action source:
On row click
を選択 - Name: 任意のものでかまいません
- Action:
Custom Action
を選択 - Code Blockに以下を貼り付け
// ref. https://stackoverflow.com/questions/73500109/thingsboard-how-to-change-an-attribute-from-current-user-using-the-custom-actio
const userId = widgetContext.currentUser.userId;
const userEntityId = {entityType: "USER", id: userId};
// Get attributeService
// ref. https://hackmd.io/@Vian/B1tcUbFuT
const attributeService = widgetContext.attributeService;
// Save attribute
const attributes = [{key: "filterDevice", value: entityLabel}];
attributeService.saveEntityAttributes(userEntityId, 'SERVER_SCOPE', attributes).
subscribe(
(res) => {
console.log(`Save ${entityLabel} to the current user attribute`);
},
error => {
console.log(error);
}
);
設定できたらDashboardを保存します。
ViewモードでEntities Table WidgetのdeviceをClick/Tapすると、今ログインしているユーザのServer attributesに Key: filterDevice
が現れ、Value
として選択したデバイス名が表示されます。Click/Tapするたびにこの値は切り替わります。
Filterを更新する
最後の工程です。User Attributeの値とFilterの値を関連づけます。
さきほど作成したFilterの設定画面を開き、下図の {x}
をクリックします。
Dynamic source type
に Current User
を、 Source attribute
に filterDevice
を指定します。
ついでに User parameters
を開いて Editable
のチェックを外しておきます。 User AttributeとFilterの値を同期させようとしているわけですが、その状態でテキスト入力による値の変更をしてしまうとその同期が壊れてしまうので、テキスト入力を無効化します。
忘れずにDashboardを保存します。
確認
以上で設定は終わりです。冒頭の動画のようにDevice一覧WidgetをClick/Tapしたら、Line Chartに表示されるDeviceも切り替わるはずです。
補足
副産物的ではありますが、User Attributeを使ってフィルターの値を設定するのに「選択式がわかりやすい」以外のUI/UXのメリットが一点あります。「値が保存される」ことです。
テキスト入力の段階だと、ダッシュボードにアクセスするたびにフィルターのデフォルト値(本記事では device1
)が設定されるため、 device2
が見たいのであれば都度 device2
と入力する必要がありました。しかし、選択式のフィルターではその値がUser Attributeに保持されるため、ダッシュボードにアクセスし直すと前回フィルターした値がそのまま残っています。逐一テキスト入力する必要がなくなったわけです。
念のためですが、このUser AttributeはThingsBoardのUserに紐づくので、同じUserを複数人で使い回してたら、直前にダッシュボードを閲覧した人のフィルター条件がそのまま引き継がれますし、同時に閲覧していたら変な具合になってしまうかもしれません。
まとめ
以上がDashboardでのFilterの使い方と、Advancedな選択式フィルターの設定方法でした。かなり複雑だったかと思います。他力本願ですが、このあたりの煩雑さが解消されることをお祈りしています。
Discussion