【Python】Switchbot Hub2の気温・湿度をCloudWatchに定期的に送信する【AWS Lamba】

季節の変わり目ということで、自宅の気温と湿度をCloudWatchに保存したくなりました。

そこで最小限の労力でPythonを書いて、Switchbot ハブ2の気温と湿度（とついでに照度）をCloudWatchに送信しようと思います。

さらにEventBridgeとLambdaで5分ごとに定期実行します。

![dashboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/ab3df0bc-b206-97f9-9ea0-509137916abb.png)

# GitHubリポジトリ

全てのソースコードを下記で公開しています。

https://github.com/tippy3/switchbot-hub2-custom-metrics

# #1 Switchbot API用のトークンを取得する

まずは事前準備です。

[公式サポートページ](https://support.switch-bot.com/hc/ja/articles/12822710195351-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E5%8F%96%E5%BE%97%E6%96%B9%E6%B3%95)の手順で「トークン」と「クライアントシークレット」を取得します。Switchbotアプリでバージョンを10回タップすると現れます。

# #2 Switchbot Hub2のデバイスIDを取得する

SwitchbotアプリでHub2を選び、「設定」→「デバイス情報」で「**BLE MAC**」の値を確認します。そこからコロンを除いた値がデバイスIDです。

例: `A1:BC:23:D4:5E:F6` → `A1BC23D45EF6`

▼参考

https://yoshiori.hatenablog.com/entry/2021/02/27/201049

# #3 AWS Secrets Managerにトークン類を保存する

ソースコードにトークン類を書くのはよくないので、AWS Secrets Managerに保存します。

AWS マネジメントコンソール → Secrets Manager → Store a new secret

**シークレット名: `switchbot`**

|Key|説明|
|---|---|
|`token`|#1で取得したトークン|
|`secret`|#1で取得したクライアントシークレット|
|`device_id`|#2で取得したデバイスID|

![secrets_manager.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/ccfd752b-cc38-dca0-aa72-2af938a72e58.png)

# #4 Switchbotからデータを取得する

ここからコーディングです。この章では次のことをします。

1. Secrets Managerからトークン類を取得する
2. 1のトークン類を使ってSwitchbot APIを叩き、データを取得する
3. 手元で実行してみる

### #4-1 Secrets Managerからトークン類を取得する

公式ドキュメントに従ってSDKを使います。

https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_cache-python.html

token, secret, device_idを取得する関数を雑に作りました。キャッシュの設定はデフォルトのままです。

```一部抜粋 app/switchbot.py
def _get_secrets():

  client = botocore.session.get_session().create_client('secretsmanager')
  cache_config = SecretCacheConfig()
  cache = SecretCache( config = cache_config, client = client)

  secret_string = cache.get_secret_string('switchbot')
  secret_json = json.loads(secret_string)
  device_id = secret_json['device_id']
  token = secret_json['token']
  secret = secret_json['secret']

  return device_id, token, secret
```

### #4-2 Switchbot APIを叩いてHub2の気温・湿度・照度を取得する

執筆時点でSwitchbot APIはv1.0とv1.1の両方が使えますが、認証がよりセキュアになったv1.1を使いましょう。

READMEの「How to Sign?」の章にPython3のサンプルがあり、コピペするだけで動くのでカンタンです。

https://github.com/OpenWonderLabs/SwitchBotAPI#how-to-sign

今回使うAPIは **GET** `https://api.switch-bot.com/v1.1/devices/{device_id}/status` です。

```一部抜粋 app/switchbot.py
def get_data():

  device_id, token, secret = _get_secrets()

  url = f'https://api.switch-bot.com/v1.1/devices/{device_id}/status'

  apiHeader = {}

  nonce = uuid.uuid4()
  t = int(round(time.time() * 1000))
  string_to_sign = '{}{}{}'.format(token, t, nonce)
  string_to_sign = bytes(string_to_sign, 'utf-8')
  secret = bytes(secret, 'utf-8')
  sign = base64.b64encode(hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest())

  apiHeader['Authorization']=token
  apiHeader['Content-Type']='application/json'
  apiHeader['charset']='utf8'
  apiHeader['t']=str(t)
  apiHeader['sign']=str(sign, 'utf-8')
  apiHeader['nonce']=str(nonce)

  response = requests.get(url, headers=apiHeader)
  response_body = response.json()['body']

  return response_body
```

ここまでのコードは [app/switchbot.py](https://github.com/tippy3/switchbot-hub2-custom-metrics/blob/main/app/switchbot.py) (GitHub) にまとめました。

### #4-3 手元で実行してみる

先にターミナルにAWSの認証情報を設定します。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html

もっとも手っ取り早くIAM Userを使う例は下記です。

```bash
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_DEFAULT_REGION=ap-northeast-1 # 東京リージョン
```

さっそく実行してみます。

```bash
python app/switchbot.py                 
{'deviceId': 'XXX', 'deviceType': 'Hub 2', 'hubDeviceId': 'C3CC01A04BF3', 'humidity': 73, 'temperature': 28.2, 'lightLevel': 1, 'version': 'V1.0-0.8'}
```

無事に気温・湿度・照度が取れました！

「`aws_secretsmanager_caching`がないよ」などと出たら`pip install`してください。

# #5 CloudWatchにカスタムメトリクスを送信する

次に、気温・湿度・照度を引数で受け取ってCloudWatchに送る関数を作ります。

ベースにした公式ドキュメントはこちらです。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudwatch/client/put_metric_data.html

```一部抜粋 app/cloudwatch.py
def post_data(temperature, humidity, light_level):

  namespace = 'Switchbot'
  dimentions = [
    {
      'Name': 'DeviceName',
      'Value': 'hub2'
    }
  ]
  metric_data = [
    {
      'MetricName': 'Temperature',
      'Value': temperature,
      'Unit': 'Count',
      'Dimensions': dimentions
    },
    {
      'MetricName': 'Humidity',
      'Value': humidity,
      'Unit': 'Percent',
      'Dimensions': dimentions
    },
    {
      'MetricName': 'LightLevel',
      'Value': light_level,
      'Unit': 'Count',
      'Dimensions': dimentions
    },
  ]

  cloudwatch = boto3.client('cloudwatch')

  cloudwatch.put_metric_data(Namespace = namespace, MetricData = metric_data)
```

この関数を [app/cloudwatch.py](https://github.com/tippy3/switchbot-hub2-custom-metrics/blob/main/app/cloudwatch.py) (GitHub) のようにまとめ、実行してみます。

```bash
python app/cloudwatch.py
success
```

AWS マネジメントコンソール → CloudWatch → All Metrics を開き、送信したメトリクスを探します。ちゃんと送信されてますね！

![custom_metrics.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/2ec842cb-6c89-b3fb-73b0-98eb6561dc1f.png)

# #6 lambda_handlerを作る

あとは#4と#5を繋げる [app/main.py](https://github.com/tippy3/switchbot-hub2-custom-metrics/blob/main/app/main.py) (GitHub) を作ります。

このあとLambdaで動かすために、関数の引数でevent, contextを受け取るようにします（受け取るだけで使いません）

```app/main.py
import cloudwatch
import switchbot

def lambda_handler(event, context):
  data = switchbot.get_data()
  temperature = data['temperature']
  humidity = data['humidity']
  light_level = data['lightLevel']
  cloudwatch.post_data(temperature, humidity, light_level)

if __name__ == "__main__":
  lambda_handler(None, None)
  print('success')
```

実行しましょう。

```bash
python app/main.py      
success
```

コーディングはここまでです。3つのファイルが完成しました。

```
.
└── app
    ├── cloudwatch.py
    ├── main.py
    └── switchbot.py
```

# #7 Lambdaで動かす

この章の手順を一発で実行するスクリプトを[リポジトリ](https://github.com/tippy3/switchbot-hub2-custom-metrics)に置いています。お急ぎの方はご利用ください。

```
./zip.sh
./create_aws_resources.sh
```

### #7-1 zip化する

作ったファイルをzip化します。あとでこれをLambda関数にします。

```bash
zip -j function.zip app/*.py
```

次に外部ライブラリをzip化します。boto3は最初からLambdaの実行環境に入っているので、`requests`と`aws_secretsmanager_caching`を`-t`でディレクトリを指定して`pip install`します。あとでこれをLambdaレイヤーにします。

```bash
pip install requests aws_secretsmanager_caching -t python
zip -r layer.zip python
```

▼ここから余談

なお、Lambda関数とLambdaレイヤーでzipコマンドを変えているのは意図的です。

今回Lambda関数はディレクトリを作る必要がないので、zipの中身は次のようにしています。

```
.
├── cloudwatch.py
├── main.py
└── switchbot.py
```

一方Lambdaレイヤーのzipはpythonディレクトリを含めています。

```
.
└── python
    ├── requests
    │   ├── __init__.py
    │   └── ほか中身たくさん
    └── ほかにも pip install で入ったものがたくさん
```

これはLambdaレイヤーの仕様でディレクトリ名が決まっているからです。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/packaging-layers.html

![zip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/c2eba441-26f4-83b7-f6cd-c8f94202ba8f.png)

▲余談ここまで

### #7-2 IAM Roleを作る

Lambda関数につける実行ロールを作ります。

AWS マネジメントコンソール → IAM → Roles → Create Role

**IAM Role名: `switchbot-lambda-execution-role`**

Trust PolicyはLambdaだけで使えるように書きます。

```assume-role-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

インラインのPermission Policyを次の内容で作ります。Secrets managerのREAD類と`cloudwatch:PutMetricData`に加え、LambdaがログをCloudWatch Logsに送れるように権限を付けています。

```permission-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetResourcePolicy",
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret",
        "secretsmanager:ListSecretVersionIds",
        "cloudwatch:PutMetricData",
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

```

### #7-3 Lambdaレイヤーを作る

AWS マネジメントコンソール → Lambda → Layers → Create Layer

**レイヤー名: `switchbot`**

先ほど作ったlayer.zipをアップロードします。x86_64とPython 3.11にチェックを入れてレイヤーを作成しましょう。

![layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/51f141b4-6df9-acbb-6c2f-ba4c451da81f.png)

### #7-4 Lambda関数を作る

AWS マネジメントコンソール → Lambda → Functions → Create Function

**関数名: `switchbot`**

この画面ではzipをアップロードできないので、いったん空の関数を作ります。実行ロールは先ほど作ったIAM Roleを指定します。

![create_function.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/b37edaab-22a8-5c00-858d-b6b9d9038233.png)

作成後の画面でfunction.zipをアップロードします。

![upload_function_zip.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/28785a70-6344-7cd0-25f8-8f1b2adc4de0.png)

無事にアップロードされました🎉 続けて同じ画面の下の方でHandlerとレイヤーを設定します。

![edit_handler.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/d67b7216-0225-e3d5-00d6-182080098061.png)

main.py のlambda_handlerを実行したいので`main.lambda_handler`にします。

![edit_handler_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/6bee5908-0623-0a0b-fc37-a2f4f8e01017.png)

先ほど作ったレイヤーを指定します。

![add_layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/df044b55-800b-086d-2413-f131dbaad675.png)

一度テスト実行してみましょう。エディタの上にある「Test」ボタンをクリックし、出てきたポップアップで何も編集せずに「Invoke」を押してみます。

![invoke.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/c5897867-7ced-9351-52ec-f1a22a3d5373.png)

すると関数がタイムアウトしてしまいました。

```
{
  "errorMessage": "2023-09-18T04:47:41.465Z {中略} Task timed out after 3.04 seconds"
}
```

そこでタイムアウトを15秒に伸ばします。ついでにメモリも256MBに増やしておきます。

![edit_timeout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/b8e26fa4-724f-b883-d66e-613a4be47aa0.png)

再度関数を実行すると、今度は正常終了しました！

```
REPORT RequestId: {中略}	Duration: 2439.00 ms	Billed Duration: 2439 ms	Memory Size: 256 MB	Max Memory Used: 102 MB	Init Duration: 714.18 ms
```

上記ログの`Max Memory Used`で最大メモリ使用量を確認できます。何回か実行してみたところ、この関数のメモリ使用量はおおむね100MB前後でした。余裕をもってメモリの設定は256MBのままにしておきます。

### #7-5 EventBridge Ruleを作る

最後にこの関数を定期実行します。関数の画面で「Add trigger」をクリックし...

![add_trigger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/9957e0ac-da77-f38e-d294-bdc909d800e3.png)

Eventbridge Ruleを選んで...

![select_eventbridge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/d13094f8-3ef0-bea6-0e8e-9498d86112f6.png)

`cron(0/5 * * * ? *)`でルールを作成しましょう。

![edit_cron.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/f79ed0ea-f8c4-f553-061b-687b41fde9ea.png)

なお、このルールからの実行を許可する Resource-based policy は自動で追加されているので気にしなくて大丈夫です。

これで5分ごとに実行されるようになりました。動かない場合はCloudWatch Logsに出力されたログを確認してみてください。

# 結果

自宅の気温と湿度がCloudWatchに保存されている〜〜〜！🎉

![dashboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502255/b54646b7-73cb-5550-b39b-16c80ae7d0b8.png)

Switchbotアプリでも「気温がN℃以上になったら」という条件は作れますが、このメトリクスを使ってCloudWatchアラームを作ることで、より高度な条件を実現できそうです。

最後までお読みいただきありがとうございました。よろしければ「いいね」もお願いします！