ラズパイ×Amazon TimeStreamで部屋の温湿度を可視化

最近ラズパイに入門してさらなる有効活用にチャレンジしました。

何番煎じかわかりませんが、私が寝室として使っている和室の温度と湿度を計測して可視化してみます。

可視化のためのソリューションとして目をつけたのが**AWS IoT Core**と**Amazon TimeStream**です。

AWSのDBと言えばDynamoDBですが、時系列データを扱うにはベストプラクティスにもあるとおり一定単位（例えば日付）でテーブルを分割、その中でタイムスタンプを主キーとして構成するような使い方が望まれます。

個人でのデバイス利用でそこまで労力はかけたくないので、若干の出費はありますが時系列管理にもってこいのAmazon TimeStreamを試してみました。

また可視化（グラフ描画）ですが、最初はQuickSightを試しましたがTimeStreamとのうまい連携方法が分からず、**AWS Lambda**と**Chart.js**で書くことにしました。

## 環境
### 利用プラットフォーム
デバイス：ラズベリーパイ4 ModelB
温湿度センサ：DHT22
メッセージング：AWS IoT Core
データベース：Amazon TimeStream
可視化バックエンド：API Gateway、Lambda
可視化フロントエンド；Chart.js

### 構成図
![無題のプレゼンテーション (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/ca327fa2-5d95-e9bd-43d1-70be971b514b.png)

各所で必要なプログラムはGitHubにあげています。
https://github.com/quotto/raspberrypi-and-aws_iot

## Amazon TimeStreamの設定
### TimeStreamの概要
はじめにラズパイから送信される温湿度データを保存する**Amazon TimeStream**を設定していきます。

TimeStreamは名前のとおり時系列データをシンプルに扱えるデータベースです。**「アクセス頻度が高い直近のデータはインメモリに、一定期間が経ったら低コストの磁気ディスクに移す」**という作業も自動でやってくれます。

テーブル構成は従来のRDBに近く、DynamoDBではサポートされていない**SQLが使える**のもポイントです。（ただし独自の関数もあるので[ガイド](https://docs.aws.amazon.com/timestream/latest/developerguide/reference.html)を参照しましょう。）

### TimeStreamでデータベースとテーブルを作る
TimeStreamを構成する要素は

- データベース
- テーブル

の2つです。これらを作っていきます。なお当記事執筆時点ではus-east-1、us-east-2、us-west-1、eu-west-1の4つのリージョンでのみ使えます。

マネジメントコンソールでTimeStreamを開き「CreateDatabase」からデータベースを作成します。
![2020-12-10_22h35_38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/c7cf8393-501a-4bc2-ff37-dee6d0ccfd43.png)

データベースの作成タイプを選択します。1から自分で作成する場合は**「Standard database」**を選択します。ちなみに「Sample database」を選択すると、IoTとDevOps用途での使用を想定した、サンプルテーブルが格納されたデータベースが作成されます。

![2020-12-10_22h40_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/db0df3ab-04e0-9e8c-4131-807aa841b483.png)

TimeStreamはKMSによる暗号化が必須ですが、Keyを指定しなければ自動で作成されるので今回は特に指定しませんでした。
![2020-12-10_22h41_47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/313809a3-dccb-9d04-c787-2be0800190d6.png)

「CreateDatabase」を実行するとすぐにDBが作成されました。
![2020-12-10_22h42_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/f5b7713d-641c-3b2b-8c13-2a3bc70a888c.png)

続いてテーブルを作成します。
![2020-12-10_22h42_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/fc7bcc4a-9c58-8dae-fd94-b435ca49a8e0.png)

先ほど作成したDBを指定して、テーブル名を入力します。
![2020-12-10_22h42_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/a6e5e1f1-f86d-ea5a-0081-4ab8bffb0209.png)

**「Data retention」**ではテーブル上のデータをメモリに保管する期間と磁気ディスクから削除する期間を指定します。この指定によりアクセス頻度が高い最近のデータはメモリに、古いデータは磁気ディスクに自動で移し替えられコストとパフォーマンスを最適化できます。
![2020-12-10_22h43_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/950572af-a25a-0573-81e4-0779eeae4b72.png)

ここまで指定したら「Create Table」を実行します。テーブルが作成されました。
![2020-12-10_22h43_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/92631da6-d040-2da4-fb67-8db0812e9881.png)

## AWS IoT Coreの設定
AWSのIoT系サービスのハブとして動くのが**AWS IoT Core**です。今回はIoT Coreの

- デバイス（モノ）
- 証明書
- ポリシー
- ルール

を構成する必要があります。

### セットアップ
一つずつ作ってもいいのですが、サクッと試す程度であればまとめて作ってしまったほうが楽です。

マネジメントコンソールのIoT Coreのトップページ左側の**「オンボード」→「開始方法」→「開始方法」**をクリックします。次のページでも**「開始方法」**をクリックします。（AWS IoT系サービスの日本語訳がちょっと変でわかりずらい……）
![2020-12-10_23h11_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/63cf948f-569f-df15-8f6e-b9761b50c7f0.png)
![2020-12-10_23h16_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/55941845-d710-75d0-7532-d67bd67a797b.png)

次の画面でこのIoT Coreに接続予定のデバイスのプラットフォームを選択します。この選択値は提供されるサンプルプログラムがわかるだけなのでで、何でもいいです。（今回は使いません）
![2020-12-10_23h26_27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/bcbdd715-c44b-cd02-9d27-75e30243fa20.png)

デバイスを識別する名前を入力し、次に進みます。
![2020-12-10_23h26_43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/33b9ffe8-60c3-a246-2707-ce7e31c10cfe.png)

ここまでくると**ポリシー**と**クライアント証明書**が生成されるので、クライアント証明書をダウンロードします。**AWSのルート証明書、クライアント証明書、秘密鍵**が格納されています。（この他にサンプルプログラムも含まれます）
![2020-12-10_23h29_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/e86667c9-1b98-ad74-99fd-ceced4e79926.png)

これらをデバイスの認証に利用することで、デバイスがセキュアに外（IoT Core）のネットワークとつながることができます。

&nbsp;  
この後に接続テストの手順が表示されますが、今回は省略します。

### ルールを作ってIoT CoreとTimeStreamをつなぐ
ここまででIoT Core上にモノ、証明書、ポリシーが出来上がりました。最後に**ルール**を作成し、デバイスからIoT Coreで受信したメッセージ（トピック）をTimeStreamのテーブルに格納するように設定します。

IoT Coreの画面左側**「ルール」**から作成します。まずはこのルールの名前を入力します。
![2020-12-10_23h32_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/6cd13285-5c08-d6c8-2dc5-6484eea949b2.png)

![2020-12-10_23h32_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/dff81a7d-1100-0288-e6a3-82edc507263b.png)


次に**ルールクエリステートメント**を設定します。これはIoT CoreからTimeStreamに連携する対象データの条件をSQL形式で定義します。

この例では`topic/raspberrypi`という名前のトピックにpublishされたメッセージがすべて格納されますが((トピック名の「topic/」は自動で付加されるため、実際にpublishで指定するトピック名は「raspberrypi」になる。))、例えばトピック内のデータ項目で条件付（where）したり、`topic/#`という感じでトピック名にワイルドカードが使えたりします。
![2020-12-10_23h33_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/4b31d17b-627e-97e1-5fa0-5760894d996e.png)


続いての**アクション**では、このルールに当てはまったデータの格納先を設定します。TimeStreamを選ぶと先ほど作ったDBとテーブルが表示されるのでそれを選択します。
![2020-12-10_23h33_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/0ca249ba-01c5-a052-702e-c93ae7d84702.png)


TimeStreamにデータを格納するには**「ディメンション」**と**「タイムスタンプ」**を設定します。
![2020-12-20_21h38_41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/597473ae-229e-f100-4196-3f74fde6c398.png)


**ディメンション**は各データのメタ情報のようなもので、例えばそのデータを送信したデバイスのIDやデバイスの地域を設定します。値は固定値の他にトピックの名前やペイロード中のデータを指定できます。

**タイムスタンプ**は名前のとおり、このルールが起動した時刻情報です。単位はどこまでサポートされているのか分かりませんが今回はデフォルトのミリ秒にしておきます。

最後に**ロールの作成**でTimeStreamへ書き込み権限のあるロールを作成します。（ロール名を入力するだけで自動で作成されます）
![2020-12-10_23h35_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/76c15369-91b1-1f55-947b-11a3532e418f.png)


これでラズパイからトピック`topic/raspberrypi`にメッセージが配信されるとディメンションdeviceIDとタイムスタンプを付けてTimeStreamにデータが格納されるようになりました。

## ラズパイから温湿度をIoT Coreへ送信する
実際のデータを取るため、温湿度センサーを付けたラズパイからIoT Coreに向けて温湿度データを送信します。

### 温湿度センサーの取り付け
ラズパイに温湿度センサーモジュールを取り付けます。今回購入した[DSD TECH DHT22](https://www.amazon.co.jp/gp/product/B07CM2VLBK/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1)は接続用ケーブルが付いており、直接ラズパイのピンに差し込めば良いので手軽で簡単です。

こんな感じになりました。センサのデータはDATピンから受け取ります。ラズパイ側のGPIOは23番です。

![P_20201204_002225 - コピー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/8bdc8203-bea7-61ab-2835-de1a26e16c0b.jpeg)

![P_20201204_002229 - コピー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/18bce66c-25dc-50f4-f329-fa6e617c2206.jpeg)

### Pythonプログラムで温湿度データを取り出す
DTH22からデータを取り出すにはいくつかのPythonライブラリがありますが、今回は[CirculcuitPython](https://learn.adafruit.com/dht-humidity-sensing-on-raspberry-pi-with-gdocs-logging/python-setup)を使いました。

pythonライブラリと必要パッケージをインストールします。

```
pip3 install adafruit-circuitpython-dht
```
```
sudo apt install libgpiod2
```

実際のpythonプログラムからは、こんな感じで簡単に温度と湿度が取得できます。

```python

import adafruit_dht
import board

#DHT22のDATを接続したGPIOピン番号を指定してセンサーを取得
dht_Device = adafruit_dht.DHT22(board.D23)
# 温度の取得（摂氏）
dht_Device.temperature
# 湿度の取得
dht_Device.humidity
```
### IoT Coreに温湿度を送信する

最後に上記のプログラムで取得した温湿度データをIoT Coreに送信します。

pythonではAWS IoT関連のsdkが用意されているのでまずはライブラリをインストールします。

```shell
pip3 install awsiotsdk
```

IoT Coreへのメッセージ送信はAWSが公開しているサンプルプログラムが役に立つのでとりあえずこれをcloneします。

```shell
git https://github.com/aws/aws-iot-device-sdk-python-v2.git
```

IoT Coreの作成でダウンロードしたクライアント証明書、秘密鍵、ルート証明書をラズパイ上の適当なディレクトリに配置します。

```shell
scp -r ./download/certificate/* pi@raspberrypi.local:~/certs
```

&nbsp;  

サンプルプログラムをもとに引数を受け取ってトピック`topic/raspberrypi`にJSONメッセージを送信するプログラムを作成します。

ソース全部は[GitHub](https://github.com/quotto/raspberrypi-and-aws_iot)を参照してもらうこととして、必要な値を引数に指定してスクリプトを実行します。

```shell
python publish.py --endpoint xxxx.xxxx.xxxx --topic raspberrypi --root-ca ~/cert/root-CA.pem --cert ~/cert/cert.pem --key ~/cert/private.key.pem --count 1
```

`--endpoint`がメッセージのpublish先になりますが、これはIoT Coreのコンソールから確認することができます。
![2020-12-27_20h40_47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/09cfce41-7f69-246f-475d-f498cf3877bd.png)

また注意点として、IoT Core上のルールやポリシーのトピック名は`topic/raspberrypi`になりますが、「topic/」部分はIoT Core上で自動で付与されるためSDKプログラムで指定するトピック名は「raspberrypi」になります。

&nbsp;  
正常に実行されると次のようなメッセージがIoT Coreに向かってpublishされます。

```json
{"temp": 20.5, "humidity": 45.3, "timestamp": 1607230805}
```

なお`timestamp`をつけていますが前述のとおり、TimeStreamへデータを格納する際にタイムスタンは自動で付与されます。


### TimeStreamのデータを確認する
Amazon TimeStreamのコンソールのクエリーから実際に格納されたデータをSQLで抽出できます。
![2020-12-27_20h43_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/bdcfe99f-48cd-ed0f-72a4-2c276a2cb684.png)

クエリを実行すると画面下に抽出レコードが表示されます。
![2020-12-27_20h43_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/cd640aa6-bfd8-26f4-5787-b93d20786715.png)


TimeStreamには、ラズパイがpublishしたjsonペイロードの各項目が1レコード分のデータとして**measure_name**（項目名）と**measure_value**（値）のペアで登録され、IoT Coreで設定した**ディメンション**と**タイムスタンプ（time）**が全レコード共通の項目として設定されています。

## クエリを発行してグラフを描いてみる
最後にTimeStreamに格納されたデータを取り出してグラフにしてみます。

今回はフロントエンドの静的ファイルをs3に格納し、その内部でバックエンドのapi-gateway→lambdaを叩く構成です。細かい部分は省略しますので全容は[GitHub](https://github.com/quotto/raspberrypi-and-aws_iot)を参照してください。

### TimeStreamからデータを抽出する（バックエンド）
lambdaの中でこのようなクエリを発行してTimeStreamから値を取り出しています。(${}はjavascript中の変数）

```sql
WITH temps AS (
  select time,measure_value::double as temp from"raspberrypiDB"."temp_and_humidity_table"
  where measure_name = 'temp' order by time
),humidities AS (
  select measure_value::double as humidity,time from 
"raspberrypiDB"."temp_and_humidity_table"
  where measure_name = 'humidity' order by time
)
select to_iso8601(a.time) as time,a.temp,b.humidity from temps a 
left outer join humidities b 
on a.time=b.time where a.time >= from_iso8601_timestamp('${begin_dateISOString}') 
and a.time <= from_iso8601_timestamp('${end_dateISOString}') 
order by a. time
```

このSQLは指定した時間内のデータを`timestamp`で結合して、同時刻の温度と湿度を取り出します。

TimeStreamのSQLは独自の関数が用意されており、`from_iso8601_timestamp(string)`はiso8601形式の文字列を受け取り、TimeStream上のtimestamp型に変換します。

### Chart.jsでグラフ描画（フロントエンド）
フロントエンドのJavaScript内で、バックエンドから上記のSQL結果セットを受け取り、温度と湿度の2軸折線グラフを出力します。

最終的にはこんな感じになりました！
![2020-12-20_22h03_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183744/04e41aad-0318-ba55-ba86-d293dc5dfd36.png)


データが飛び飛びなのはラズパイをつけたり切ったりしているからです。Chart.jsは初めてでグラフのバランスもちょっとおかしいですが、なんとなくそれっぽいものが作れました！！

&nbsp;&nbsp;

DynamoDBはとっても手っ取り早くて簡単に使えますが、ちょっとした時系列データを扱いたい時や、慣れ親しんだSQLが使えないという難点もありました。

Amazon TimeStreamはそのへんをうまく保管してくれる機能が揃っています。

とはいえ、個人的にはディメンションの扱い方がピンときていなかったりするので、IoTに限らず、今後も積極的に使っていきたいと思います。