sorta kinda...

主にAWS関連ですが、これに限らずいろいろ勉強したことや思ったことを書いていきます。

自宅付近の気温と湿度を可視化してみた

こんにちは〜那須です。

私の生活圏は〇〇のシベリアとか〇〇のチベットとか言われる地域で、夏は他の地域とさほど気温差は感じないのですが冬は本当に寒いです。 これできっちり雪でも降ってくれたらまだいいんですが、ほとんど雪は積もらなくて空気も乾燥しまくるのでなかなか辛いもんがあります。 Twitter 等でも毎年冬になるとどんだけ寒いのかを書いてくれる人がいらっしゃいますが、実際どれくらい寒いのか数字で知りたいと思い始めました。

天気予報を見れば済む話だとは思いますが、そもそもうちの地域の天気予報の測定地点を調べてみると何もない場所にあるっぽいので、確かにそこは寒いんですよね。 私が知りたいのは生活圏での気温や湿度なので、いちエンジニアらしく(?)家の敷地内に温湿度計を置いてそのデータを可視化することにしました。

 

測定環境

庭のどこかに温湿度計を置こうかと思ったのですが、なるべく雨に濡れない場所で測定したいので家のベランダに環境を作りました。 温度や湿度を計測する環境としては百葉箱がありますが、そんなもんをベランダに置くわけにもいかないので家にあるもので百葉箱的なものを作りました。 (ちなみに今の時代は百葉箱なんて使わないそうです。強制通風筒ってのを使うそうです。) その中に温湿度計を入れて測定します。 作った物は↓こんな感じで設置しました。

f:id:nasrinjp1:20201215162920j:plain
f:id:nasrinjp1:20201215163139j:plain

百均で売ってるようなプラスチックのカゴにビニール紐を通してぶら下げてるだけです。 直射日光はほとんど当たらない場所なんですが、一応当たっても大丈夫なように牛乳パックを被せてます。 自由研究っぽさが出てていいですね。 普段あまり工作はしないのでこれを作るだけでも十分楽しめました。

めちゃくちゃベランダの地面に近いんですが、本当は↓こんな感じで目の高さくらいにぶら下げたかったんですよね。

f:id:nasrinjp1:20201215163221j:plain

ただやってみると、こんな感じでアンテナピクトがほぼ 0 になるんです。 この真下の蛇口にぶら下げるとなんの問題もないです。 照明の近くがダメなのか照明についている人感知センサーの近くがダメなのか、どちらかしか原因としては思いつかないですが細かいことは気にしません。 これでもデータは送信できてるので問題はないんですが、いつかデータ送信に失敗しそうな気がしたのでさっきの写真のようにしています。

f:id:nasrinjp1:20201215161825p:plain

 

準備した温湿度計

GPS マルチユニット SORACOM Edition(バッテリー内蔵タイプ)スターターキットを購入しました。
soracom.jp

温湿度計は他にもいっぱいあったのですが、

  • そういえば SORACOM って名前をよく見る
  • なんか簡単にデータを送信できて簡単にクラウド連携できるらしい
  • ブログ等で情報収集しやすそう

という理由でこれにしました。 13,000 円くらいしたので、元を取るためにも勉強しようと思いました。

 

可視化できるまで

全体構成

このような構成にしました。
f:id:nasrinjp1:20201215222901p:plain

IoT Core から Timestream に直接データを入れることはできるのですが、諸事情により間に Lambda を挟んでいます。 理由はハマったポイントにまとめています。
あと、Funnel は Public Beta のサービスみたいです。 AWS 同様にまだ本番では使わない方がいいのかもしれません(リリースからもうすぐ 5 年経つみたいですがどうなんだろうか)

 

設定の流れ

ほぼ↓この記事の内容の通りに設定するだけで終わりました。 この記事がなかったら途方に暮れていたと思います。

blog.soracom.com

途中で認証情報として IAM ユーザのアクセスキーとシークレットキーを登録するところがあるんですが、IAM ロールでやりたいなと思って SORACOM サポートに聞いてみたら、「IAM ロールはサポートされてないです〜」と回答がありました。 なので素直にアクセスキーとシークレットキーを登録しましょう。

 

注意点

Funnel で指定する転送先 URL だけ注意しましょう。 ルールトピックは $aws/rules/ruleName です。上記の記事のテキストでは rules の s が抜けてるので、そのままコピペすると転送できません(エラーの内容をメモするの忘れてました…

docs.aws.amazon.com

あと、timestamp のユニットは MILLISECONDS にしましょう。 秒未満の情報はいらないと思って SECONDS を指定したら IoT Core で以下のエラーが出ました。

[ERROR]  EVENT:TimestreamActionFailure TOPICNAME:$aws/rules/ルール名/data/device1 CLIENTID:N/A MESSAGE:Failed to write records to Timestream. The error received was 'Invalid time for record. (Service: AmazonTimestreamWrite; Status Code: 400; Error Code: ValidationException; Request ID: XXXXXXXXXXXXXXXXXXXXXXXXX; Proxy: null)'. Message arrived on data/device1, Action: timestream, Database: DB名, Table: テーブル名

 

ハマったポイント

事象確認

Timestream に温度と湿度のデータを入れる際にちょっと困った事象が発生しました。 とりあえず何も考えずに、温度、湿度、電池、アンテナの 4 つのデータを Timestream に初めて送信した時の Timestream のテーブルの内容です。

f:id:nasrinjp1:20201215214812p:plain

なんか温度だけデータがある時刻とない時刻がある...?

 

状況調査

データが取れてないのかなーと思って SORACOM Harvest で確認してみました。

f:id:nasrinjp1:20201215173327p:plain

うーん、データは取れてますね。 IoT Core からのログを見ると、以下のエラーが出てました。

[ERROR]  EVENT:TimestreamActionFailure TOPICNAME:$aws/rules/ルール名/data/device1 CLIENTID:N/A MESSAGE:Failed to write records to Timestream. The error received was 'One or more records have been rejected. See RejectedRecords for details. (Service: AmazonTimestreamWrite; Status Code: 419; Error Code: RejectedRecordsException; Request ID: XXXXXXXXXXXXXXXXXXXXXXXXXXX; Proxy: null), Rejected records: [{RecordIndex: 0,Reason: Measure name already has an assigned measure value type. Each measure name can have only one measure value type and cannot be changed.}]'. Message arrived on data/device1, Action: timestream, Database: DB名, Table: テーブル名

なんか measure value type が違うよ的なことを言ってますね。

…あれ? Timestream のデータを見ると最初の温度データが 14 と整数で入ってる… どうやら一番最初のデータが整数だったから温度は bigint でスキーマ登録されてしまったみたいです。 小数のままだったら問題なかったのに、どこで整数になってしまうんでしょうか。 SORACOM Harvest でデータの JSON を確認するとこうなってました。

f:id:nasrinjp1:20201215175128p:plain

一次処理済みデータは小数表示ですね。 グラフ用データは整数になっています。 なんとなく SORACOM の仕組みのどこかで整数になってる気がしたので、SORACOM サポートに問い合わせてみました。

 

SORACOM サポートに問い合わせた結果

わりとすぐにサポートの方から回答が返ってきて、SORACOM Funnel から IoT Core に送信される時に整数になっているとのことでした。 普段から AWS サポートの素晴らしさを感じていましたが、SORACOM サポートも同じくらいすばらしかったです! ソラコムさんありがとうございます! 助かりました!

ということで、IoT Core に届いた時には .0 の数値データはすでに整数になっているということです。 これを .0 つきの小数データにしてあげれば OK ですね。

 

どう対応したか

まず Timestream は一度 measure value type が定義されてしまうとそのまま固定されます。 なので一度テーブルを削除して再作成するか、別テーブルを作成してやり直すか、同じテーブルでmeasure_name を変更してデータを送信する必要があります。 今回は別テーブルを作成してそちらにデータを送信するようにしました。 ついでにmeasure nameも変えてみました。

整数を小数に変換するやり方ですが、最初は IoT Core に定義したルールクエリで cast しようと思いました。

docs.aws.amazon.com

ただ、Int データを Decimal に cast しても値は「The source value.」とドキュメントに記載があるので、ルールクエリではどうしようもなさそうです。

というわけで、IoT Core から Timestream にデータ送信する前に、Lambda で整数を小数にして Timestream に送信するようにしました。 ついでに電池の残量が少なくなったら Slack に通知するようにしました。 電池残量も Timestream に送信して可視化しようと思ったんですがなんかちょっと違うなと思ったので、この Lambda で電池関連の処理は全て完了させています。

import json
import boto3
import os
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from logging import getLogger, INFO

logger = getLogger()
logger.setLevel(INFO)

database_name = 'DB名'
table_name = 'テーブル名'
client = boto3.client('timestream-write')


def lambda_handler(event, context):
    # Write to Timestream
    timestamp = event['timestamp']
    dimensions = [
        {'Name': 'deviceId', 'Value': event['imsi']},
        {'Name': 'operatorId', 'Value': event['operatorId']}
    ]
    records = []
    records.append(prepare_record('temperature', event['temp'], 'DOUBLE', dimensions, timestamp))
    records.append(prepare_record('humidity', event['humi'], 'DOUBLE', dimensions, timestamp))
    write_records(records)

    # battery check
    if event['bat'] == 3:
        logger.info("The battery is fully charged.")
    elif event['bat'] == 2:
        logger.info("The battery is halfway gone.")
    elif event['bat'] == 1:
        logger.info("The battery is low.")
        attachments_json = [
            {
                "color": 'danger',
                "title": 'GPS Multiunit SORACOM Edition needs to be charged!',
                "text": "The battery is low. It needs to be charged."
            }
        ]
        slack_message = {'attachments': attachments_json}
        notify_slack(slack_message)


def prepare_record(measure_name, measure_value, measure_value_type, dimensions, timestamp):
    record = {
        'Time': str(timestamp),
        'TimeUnit': 'MILLISECONDS',
        'Dimensions': dimensions,
        'MeasureName': measure_name,
        'MeasureValue': str(measure_value),
        'MeasureValueType': measure_value_type
    }
    return record

def write_records(records):
    try:
        result = client.write_records(DatabaseName=database_name,
                                        TableName=table_name,
                                        Records=records,
                                        CommonAttributes={})
        logger.info(f'The records are written into Timestream ({database_name}.{table_name})')
    except Exception as err:
        logger.error(f"Error: {err}")

def notify_slack(slack_message):
    parameter_store_name = os.environ["parameter_store_name_for_slack_url"]
    slack_url = get_slack_url(parameter_store_name)
    req = Request(slack_url, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted")
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

def get_slack_url(parameter_store_name):
    ssm = boto3.client('ssm')
    return ssm.get_parameter(
        Name=parameter_store_name,
        WithDecryption=True
    )["Parameter"]["Value"]

 

結果

.0 データも小数のまま Timestream に届いていますね。めでたしめでたし。

f:id:nasrinjp1:20201215181520p:plain

電池がなくなりそうになった時も Slack にちゃんとメッセージが送られてきました。めでたしめでたし。

f:id:nasrinjp1:20201215212238p:plain

 

最後に Grafana で可視化

いよいよ可視化です。 Timestream にデータを入れて満足してしまいそうになりましたが、目的は可視化なのでここでグラフにしてパッと見で気温と湿度の動きがわかるようにしましょう。 ざっと調べた感じだと Timestream と Grafana の組み合わせが多い感じなので、右に倣えで Grafana を使うことにしました。

EC2 インスタンスに Grafana をインストールするしかないかなーと思ってましたが、Grafana Cloud という SaaS があることに気付きました。 しかも Free プランでタダです。 これを使わない手はないですね。

grafana.com

サクッと Grafana Cloud でグラフ(ダッシュボード)を作ってみました。 これで本当に自分の家の周りが冬はクソ寒いのかどうかを確認したいと思います。 クエリは以下のようにシンプルにしています(温度の例)

SELECT deviceId, CREATE_TIME_SERIES(time, measure_value::double) as temperature FROM $__database.$__table
WHERE measure_name='temperature'
GROUP BY deviceId

f:id:nasrinjp1:20201217135214p:plain

グラフの右端近くの直線になっているところはデータが送られてきてませんでしたが、どうやらたまたま発生した Funnel の障害が原因だったみたいです。 個人利用なので「まあそういうこともあるわな」という感じで軽く受け止めてますが、業務でこうなったらどうすればいいのかは考えてシミュレーションしておこうと思います。 昔からそうですが、相変わらずトラブルの類との遭遇率は高いですw

 

さいごに

紆余曲折を経てしまいましたが、なんとか自分の家の周りの気温と湿度をグラフにして動きを見ることができるようになりました。 IoT のことは全く知らず何もかもが初めてで、とりあえずモノを購入して情報集めながら設定してトラブルシューティングしてでなんやかんやで大変でした。 でも実際にやってみることで IoT の基礎の基礎は分かったような気がします。 今回購入した GPS マルチユニット SORACOM Edition は加速度センサーもついてるし位置情報も取れるので、次は緯度経度を取得して地図にピンつけてなんかやってみようと思いました(ベランダの温湿度は計測し続けたいのでもう一つ買うか…

いろんな AWS アカウントから Security Hub の結果を集めて社内 Slack に通知する

本当に久しぶりに記事書きました、那須です。

Japan APN Ambassador Advent Calendar 2020 の 4 日目のエントリです。 昨日は AWS 相澤さんによる APN Ambassadorってなんだ? でした。 記事内の「オススメの勉強法」にも書かれていましたが、実際に手を動かして試すと言う行動が一番自分には合っていますね。 やれる範囲はどうしても少なくなるのですが、やったことについては自信を持って説明することができる場面が増えました。

私も APN Ambassador の一人として活動していますが、最近 3 ヶ月ほど記事を書けていませんでした。 アドベントカレンダーを機に、今日は久々に技術記事を書きます。 会社のブログに記事を書きましたのでご覧ください。

www.beex-inc.com