AWS IoT SiteWise で複数デバイスのセンサーデータ統計値をグラフ表示してみた

AWS IoT SiteWise で階層をまたいだデータの参照は計算式の「入力」がポイントです。
2022.04.09

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

前回の記事では各デバイスのセンサーデータのみを可視化していました。

20-saved-dashboard-old-dashboard

しかし場合によっては、全デバイスのデータの平均値や最大値、ときには OEE(総合設備効率)なども取得して可視化したいことがあります。
そこで今回は、下位アセットモデルのデータの統計値を上位アセットのプロパティとして定義してダッシュボードに表示してみたいと思います。

前提

前提としてデバイスデータやダッシュボードは前回のものをそのまま使うことにします。
また、取得したい統計は「1 分あたりの全デバイスのクロック数の平均値」とします。

下位アセットモデルの修正とメトリクスの設定

今回は「クロック数」の平均値を取りたいので、下位モデルである各デバイスのクロック数の平均値を「メトリクス」プロパティとして取得します。

ここでいきなり余談ですが、わざわざ下位モデルで「メトリクス」プロパティを設定しなくても、上位モデル側で「下位モデルの測定プロパティ」を参照して平均計算すればいいのでは? という気がしますね。

しかし、階層をまたいでメトリクスに参照できるのは「下位モデルのメトリクス」もしくは「同階層の測定プロパティ」だけです。
そのため、今回のように下位モデルで「メトリクス」を定義しています。

01-edit-test-device-model

再び少し本題からそれますが、メトリクスを設定する前に「クロック数の定義」を修正しておきたいと思います。
理由は「元のクロック数のデータ値では桁数が大きすぎてダッシュボードに表示した際にグラフ縦軸の目盛りが見切れてしまっていた」ためです。これを「MHz」に補正する為、「定義の変換」プロパティを使って取得したデータの桁と単位を下記の通り変更しました。

02-convert-define

「定義の変換」が入力できたら、そのまま画面を下にスクロールして「メトリクス」を定義します。
下記のとおり「1 分間の各デバイスのクロック数の平均」を定義します。計算式はコピペできないので次のように入力します。

  • 「計算式」欄に avg()と入力
  • avg() のカッコの中にカーソルを移動
  • PCのキーボードの十字キーで「↓」をクリック
  • プルダウンに出現する clock_MHzを選択

03-define-metrics

保存できたら「定義を変換する」タブを見て正しく設定が反映されていることを確認おきます。
「MHz への変換」が正しい計算式でセットされていることが確認できました。

04-describe-conversion

「メトリクスの定義」タブでも正しい平均値の計算式でセットされていることが確認できました。

05-describe-metrics-definition

上位アセットモデルにメトリクスを設定

次に上位のアセットモデルである「All Device」にメトリクスを設定していきます。

06-edit-all-device-model

「メトリクスの定義」の設定箇所で下記図のようにメトリクスを登録します。
SiteWise 独自の計算式の表記ですが、この計算式の意味は次のような意味を持ちます。

  • 下位のアセットモデルのメトリクス avg_clock の平均値(1分間)を算出する
  • 下位のアセットモデルの特定には定義済みの階層定義である device_hierarchy を参照する

この計算式の設定が今回のポイントになります。
(設定方法はすぐ後に記載しています。)

07-define-all-device-metrics

さて、これは単純な計算式ではないので入力が少々難解ですが、私の環境(MacBook / Chrome)では下記の手順で入力できました。

  • 計算式の入力エリアで PC のキーボードから十字キーの「↓」を入力
  • プルダウンに現れる device_hierarchy を選択
    • device_hierarchy を選択しても計算式には何も入力されていないように見えてますが次の操作を行います。
  • そのまま再度 PC のキーボードから十字キーの「↓」を入力
  • プルダウンに現れる avg_clock を選択
    • この時点で計算式欄には device_hierarchyavg_clock が並んで入力されていれば OK です
  • 次に計算式欄のカーソルを一番左に移動させて avg( をキーボードから直接入力
  • 計算式欄の最後に ) をキーボードから直接入力

ダッシュボードの修正

ここまでの設定が完了すれば、次はダッシュボードを修正します。
まず最初に、クロック数の桁が大きいままのグラフを削除しましょう。

10-delete-clock

次に単位を「MHz」に変換したプロパティ clock_MHz をグラフに追加します。
下記では最初に test-device-1 のプロパティを追加しています。

11-device-1-mhz

同様に test-device-2 のプロパティを追加します。

12-device-2-mhz

最後に、この記事の本命である「2つのデバイスのクロック数の平均」をグラフに追加します。
上位アセットモデルで定義しているので、All Device のアセットを選択して total_avg_clock というプロパティをグラフの一番下に追加します。

13-total-avg-clock

最終的に下記のようなグラフになりました。一番下のグラフで「全デバイスのクロック数の平均」も可視化することができました。

14-complete-graph

最後に

今回は下位モデルのデータ郡から統計データを取得する方法を紹介しました。
記事中で赤文字でも記載しましたが、ポイントは「下位アセットモデルのプロパティを利用した計算式の設定」です。

コンソールから行うと非常に難解な作業になるので AWS CLI で設定したいところですが、その方法については改めて別の記事でご紹介したいと思います。

以上です。

補足

今回は分かりやすいグラフになるようにデバイスデータを生成するスクリプトを前回のものから少し変更しています。
(変更といってもグラフの山と谷になる時間を伸ばしただけです。)

コピペで使えるようにコードを記載しておきます。

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, sending_period):
    start = time.time()
    mode = 'low' # 最初はLOWモードでスタートすることにする
    logger.info("starting low mode... ")

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

        if mode == 'low':
            if timer < sending_period:
                MeasureValueTemp = random.uniform(1, 30)
                MeasureValueClock = random.randint(600000000,800000000)
                logger.info("temperature: {}".format(MeasureValueTemp))
                logger.info("clock: {}".format(MeasureValueClock))
            else:
                mode = 'high'
                logger.info("starting high mode... ")
                start = time.time() + 1  # reset timer
                continue
        else: #if mode == 'high':
            if timer < sending_period: # Mode: High load
                MeasureValueTemp = random.uniform(50, 90)
                MeasureValueClock = random.randint(1200000000,1500000000)
                logger.info("temperature: {}".format(MeasureValueTemp))
                logger.info("clock: {}".format(MeasureValueClock))
            else:
                mode = 'low'
                logger.info("starting low mode... ")
                start = time.time() + 1 # reset timer
                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))
        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,120))
device_2 = threading.Thread(target=batch_put_asset,args=(2,120))
device_1.start()
device_2.start()