AWS IoT SiteWise Monitor で複数デバイスのセンサーデータを可視化してみた

複数デバイスを SiteWise Monitor で可視化するときはアセットモデルを階層化しよう!
2022.04.08

以前の記事では、単一デバイスのデータを SiteWise Monitor で可視化しました。
もちろん複数デバイスのセンサーデータも可視化することができますが、アセットモデルの作り方に注意が必要なので、その方法をご紹介したいと思います。

アセットとアセットモデル

最初に、簡単にアセットとアセットモデルについて確認しておきましょう。
公式ドキュメントは下記になります。

AWS IoT SiteWise では「個々の機器」や「個々の工場」、「プラント全体」などを「アセット」という単位で表現することができます。

また、これらのアセットは個々に様々なデータを持ちます。例えば 発電機なら「発電量」「温度」「回転数」などです。工場全体で見ると個々の工場ごとに「合計発電量」「平均回転数」などのデータを持つことが考えられます。これらのアセットを構成する要素をアセットプロパティと呼びます。

上記のような動的なデータは「測定値」というプロパティになります。 アセットプロパティのその他の種類として「デバイスの型番などの静的な属性」や 「華氏を摂氏に変換した計算値」、「データの平均値といったメトリクス」も定義することが可能です。

このように各アセットのデータ構造を定義するフォーマットがアセットモデルです。アセットのテンプレートと考えると分かりやすいです。
下記はアセットとアセットモデルのイメージ図です。

100-asset-asset-model

またアセットモデルは階層的に構成することもできます。例えば、「個々の機器」「個々の工場」「全ての工場」という形で階層化して、デバイス単位、工場単位、全工場という単位別にデータを管理・可視化することができます。

101-asset-model-hierarchy

階層化されたアセットを可視化する

前回は単一のデバイスに対して SiteWise Monitor で可視化しました。
先程のように図にすると下記のようなイメージです。

102-single-asset-monitor

単一デバイスのときは特に問題なく可視化できますが、SiteWise Monitor のダッシュボードに登録できるのは「単一のアセットのみ」です。そのため複数のアセットをダッシュボードに登録できません。

103-multi-assets-monitor

これを解決するために、アセットモデルを階層化して最上位のアセットをダッシュボードに登録することで全体を可視化することができるようになります。

104-hierarchy-asset-model-monitor

疑似デバイスデータで試してみる

アセット、アセットモデル、モデルの階層化について概念が理解できたので、実際に擬似的なデバイスを用意して作業をしていきます。今回作成する全体像は下記の様になります。

105-sample-asset-model-hierarchy

アーキテクチャは下記のとおりです。今回も SiteWise API を使って擬似的なデバイスデータを SiteWise に書き込みます。

106-sitewise-monitor-multi-device

アセット、アセットモデル、階層化の作成

まずは下位のアセットモデルを作成しましょう。このモデルは個々のデバイスを定義するアセットモデルになります。(Test Device という名前で作成しました)

01-named-asset-model

「測定の定義」clocktemperature を図の通り作成します。

02-define-measure

次にアセットを作成します。アセットモデルは先程作成した Test Device です。 今回は 2つのデバイス用のアセットを作成します。1つ目は test-device-1 というアセットです。

03-make-assets

同じように test-device-2 というアセットを作成します。

04-make-asset-2

次に上位のアセットモデルを作成します。今回は All Device という名前で作成しました。

05-make-model-all-device

「階層の定義」 で階層関係を設定します。下記のように適当な階層名を付けて「階層モデル」ではプルダウンから下位のモデルとして作成した Test Device を選択します。

06-define-hierarchy

モデルが作成できたので次にアセット All Device を作成します。

14-make-asset-all-device

先程作成したアセット All Device ができたら「編集」をクリックしてアセットの関連付けを行います。

15-edit-asset-all-device

下記のように、このアセットに関連付ける下位のアセットを指定します。「階層」には先程作成したものを選択します。また、今回は2つのデバイス test-device-1test-device-2 を関連付けます。

16-associated-assets

ここまで作業が終われば、下記のようにアセットが階層化された状態で表示されるようになります。

17-list-assets

ポータルの作成

次にデータを可視化するためのポータルを作成します。
適当なポータル名(今回は AllDevices)を設定します。「ユーザー認証」は前回と同様に「IAM」を選択しました。

20-config-portal

「サポート連絡先のEメール」は適当なものをセットして下さい。
「アクセス許可」のロールは新規作成することにします。

21-mail-and-next

次のオプションでは、前回同様に「アラームの有効化」と「エッジ設定」を無効にしておきます。

22-option

管理者も前回同様に既存の IAM Role を選択しました。

23-select-iam-role-and-admin

ユーザーは未設定のままとしました。
(検証で利用者が私しかいないので)

24-assign-user

AllDevices というポータルが作成できました。

25-all-device-portal

ダッシュボードの作成

ポータルができたので「ポータルを開く」をクリックして SiteWise Monitor に移動します。

26-open-portal

モニターの画面でも階層化されたアセットが見えていることを確認できます。

27-nested-assets

最初に説明した通り、「ダッシュボードには 1 つのアセットしか登録できない」ので、最上位のアセットモデルのアセットである All Device を選択して「プロジェクトにアセットを追加」をクリックします。

28-add-asset-to-project

All Device を選択して「次へ」をクリックします。

29-select-asset

新しいプロジェクト名を付けてプロジェクトを作成します。これでアセットが追加された状態でプロジェクトが作成されます。

30-make-new-project

プロジェクトを作成すると、そのプロジェクト画面が開いた状態になるので、そのまま「ダッシュボードを作成」をクリックしてグラフ配置を作成していきます。

31-make-dash-board

前回のようにアセットを選択して、ダッシュボード中央にドラッグ&ドロップでグラフを配置していきます。

32-drag-drop-property

下記のように test-device-1test-debice-2 の両方のアセット(計 4 つ)が配置できました。
まだデータが来ていないのでグラフには何もプロットされていません。

33-all-device-dashboard

データの書込み

次のコードを使って 2つのデバイスを模した擬似的なセンサーデータを書込みます。

sitewise-send-data-api-thread.py

import boto3
import uuid
import random
import time
import logging
import sys
import json
import math
import threading

logger = logging.getLogger()
logger.setLevel(logging.INFO)
streamHandler = logging.StreamHandler(stream=sys.stdout)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
streamHandler.setFormatter(formatter)
logger.addHandler(streamHandler)

client = boto3.client('iotsitewise')

def batch_put_asset(device_id):
    start = time.time()
    mode = 'low' # 最初はLOWモードでスタートすることにする
    logger.info("starting low mode... ")

    while True:
        timer = int(time.time() - start)
        # 現在時刻の取得 (小数部, 整数部)
        # https://note.nkmk.me/python-math-modf/
        timestamp_float, timestamp_int = math.modf(time.time())

        if mode == 'low':
            if timer < 10:
                MeasureValueTemp = random.uniform(1, 30)
                MeasureValueClock = random.randint(600000000,800000000)
                #print('mode low')
                logger.info("temperature: {}".format(MeasureValueTemp))
                logger.info("clock: {}".format(MeasureValueClock))
            else:
                mode = 'high'
                logger.info("starting high mode... ")
                start = time.time() + 1  # reset timer
                #start = time.time()
                continue
        else: #if mode == 'high':
            if timer < 10: # Mode: High load
                MeasureValueTemp = random.uniform(50, 90)
                MeasureValueClock = random.randint(1200000000,1500000000)
                #print('mode high')
                logger.info("temperature: {}".format(MeasureValueTemp))
                logger.info("clock: {}".format(MeasureValueClock))
            else:
                mode = 'low'
                logger.info("starting low mode... ")
                #print(str(int(time.time())) + ' start low mode...')
                start = time.time() + 1 # reset timer
                #start = time.time()
                continue

        try:
            response = client.batch_put_asset_property_value(
                entries=[
                    {
                        'entryId': '{}'.format(uuid.uuid4()),
                        'propertyAlias': '/test/device/{}/temperature'.format(device_id),
                        'propertyValues': [
                            {
                                'value': {
                                    'doubleValue': MeasureValueTemp
                                },
                                'timestamp': {
                                    'timeInSeconds': int(timestamp_int),
                                    'offsetInNanos': int(round((timestamp_float * 1000000000), 0))
                                },
                                'quality': 'GOOD'
                            },
                        ]
                    },
                    {
                        'entryId': '{}'.format(uuid.uuid4()),
                        'propertyAlias': '/test/device/{}/clock'.format(device_id),
                        'propertyValues': [
                            {
                                'value': {
                                    'doubleValue': MeasureValueClock
                                },
                                'timestamp': {
                                    'timeInSeconds': int(timestamp_int),
                                    'offsetInNanos': int(round((timestamp_float * 1000000000), 0))
                                },
                                'quality': 'GOOD'
                            },
                        ]
                    },
                ]
            )
            logger.info("response: {}\n".format(json.dumps(response, indent=2)))
            logger.info("propertyAlias: {}\n".format(device_id))

            if response['errorEntries']:
                    logger.error("temperature: {} clock: {}".format(MeasureValueTemp, MeasureValueClock))
            #else:
            #    logger.info("temperature: {} clock: {}".format(MeasureValueTemp, MeasureValueClock))
        except Exception as e:
            logger.error("{}".format(e))
            logger.error("temperature: {} clock: {}".format(MeasureValueTemp, MeasureValueClock))

        time.sleep(5)

device_1 = threading.Thread(target=batch_put_asset,args=(1,))
device_2 = threading.Thread(target=batch_put_asset,args=(2,))
device_1.start()
device_2.start()

上記コードを実行する環境に対して、SiteWise に書込みできる権限 を付与しておきます。
付与する IAM 権限は下記のとおりですが、緩い内容になっているので本番用途で利用する際はさらに権限を絞って設定して下さい。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "iotsitewise:BatchPutAssetPropertyValue",
            "Resource": "*"
        }
    ]
}

スクリプトを実行して書込みをスタートします。

$ python sitewise-send-data-api-thread.py

アセットプロパティとデータストリームの関連付け

データの書込みが正常に行えていれば、SiteWise のコンソールにある「データストリーム」の画面で、下記のように4つの「データストリームエイリアス」が表れています。
4 つ全てを選択して「データストリームを管理」をクリックして、アセットプロパティと関連付けていきます。

40-manage-data-stream

最初にデータストリームエイリアス /test/device/1/temperature の「測定値を選択」をクリックします。
次に、対応するアセット test-device-1を展開して、このエイリアスに対応する測定値 temperature を選択します。

最後に左上の「選択」をクリックします。

41-device-1-temp

/test/device/1/clock に対しても同じ手順で関連付けを行います。

42-device-1-clock

test-device-2についても同じ手順を繰り返します。

43-device-2-temp

全ての関連付けが完了したら下記のようになっているはずなので、最後に「更新」をクリックして設定を確定します。

44-modify-association

正しく関連付けていることが分かります。

45-list-stream

ダッシュボードの変更

データの関連付けができれば下記のようにダッシュボードにもデータが反映されているはずです。
これで、全デバイスのデータを1つのダッシュボードで可視化することができました。

46-dashboad-all-device-plot

このままでも問題ないですが、グラフが4つに分散していて見づらいので同じ種類のグラフをまとめてみたいと思います。
グラフの描画設定を変更するには画面右上の「編集」をクリックします。

47-edit-dashboard

まず、test-device-2 のグラフを全部(clocktemperature)一旦削除します。

48-delete-device-2

次に test-device-1clock のグラフを temperature グラフの下側に移動させます。

49-deleted-graph

両方のグラフの大きさを合わせておきます。

50-streched-graph

次に、削除した test-device-2 のグラフを再度追加します。追加するときに既存のグラフ上に別のグラフを重ねることで 1 つの領域で複数のグラフを描画することができます。

52-dorag-drop-on-device-1

これで、clocktemperature という種類別に2つのデバイスをまとめてグラフ化できました。
グラフの名前も変更しておきましょう。

53-save-dashboard

変更を保存したら下記のようなグラフになりました。2つのデバイスデータを直感的に比較できるようになり見やすくなりました。

54-saved-dashboard

最後に

今回は複数データの可視化と、そのために必要となるアセットモデルの階層化についてご紹介しました。
「どのようなデータをどのように可視化したいのか」をしっかりイメージして階層を作ることがポイントになります。

以上です。