Stream Deckをぽちっとして、カーテンを開ける

ガジェットはお好きですか？

わたしは好きです。

このたび、ガジェット好きがよく持っていると噂のStream Deckを手に入れましたので、ボタンぽちっとすることで、SwitchBotカーテンを設置したカーテンを開閉してみたいと思います。

仕組みは簡単に、以下のとおりです。

![sb_curtain.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/bee11b27-cda2-ed80-6ca2-c66f868f40e1.png)

Stream DeckのプラグインにはAPIへGETとかPOSTするプラグインが存在するのに、どうして、バッチファイルやPythonプログラムをはさんでいるのか？ですが、これはSwitchBot APIの認証がv1.1になって複雑になったためです。

v1.0までは、SwitchBotアプリで生成するトークンのみでAPIへリクエストができていましたが、どうやらv1.1から、より安全性を高めるために現在時間を認証情報に含む形へ変わったようです。

正しい言い方をわたしは知らないため、各自調べてください。

https://github.com/OpenWonderLabs/SwitchBotAPI

セキュリティにまつわる複雑な話はさておいて、この変更により、プログラムによる演算を挟む必要が出てきたため、まずはSwitchBot APIを呼ぶPythonプログラムから準備していきます。

# Pythonプログラム

## デバイスID取得

こちらの「デバイスの一覧を取得する」を使わせていただき、制御したいデバイスのデバイスIDを特定します。

https://qiita.com/taiki-kuraishi/items/7739cffc79611d27045c#%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%81%AE%E4%B8%80%E8%A6%A7%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B

<details><summary>動かしたコード（引用元は上記リンク）</summary>

```python
import os
import time
import json
import hashlib
import hmac
import base64
import uuid
import requests
import json
import datetime
from os.path import exists
from dotenv import load_dotenv

load_dotenv()

dir_name = "./responses"
if not exists(dir_name):
    os.makedirs(dir_name)

token = os.environ["SWITCHBOT_TOKEN"]
secret = os.environ["SWITCHBOT_SECRET"]
nonce = str(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 = {}
apiHeader["Authorization"] = token
apiHeader["Content-Type"] = "application/json"
apiHeader["charset"] = "utf8"
apiHeader["t"] = str(t)
apiHeader["sign"] = str(sign, "utf-8")
apiHeader["nonce"] = nonce

response = requests.get("https://api.switch-bot.com/v1.1/devices", headers=apiHeader)
devices = response.json()

timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
response_file = f"{dir_name}/devices_{timestamp}.json"
with open(response_file, "w") as f:
    json.dump(devices, f)

print("Success get devices list.")
```
</details>

（トークンや秘密鍵は`.env`ファイルを作成し、そちらへ格納してあります）

そして、入手したJSONファイルをVS Codeとかでフォーマットし、
```
"deviceType": "Curtain3",
```
を探し、
```
"deviceId": "CURTAIN3XXX",
```
を見つけます。（上記のIDそのものはダミーで、本物は英数字の羅列です）

カーテンを右と左でセットにしている場合は、`OpenDirection`でカーテンの左右が判別できます。
おそらくどちらか片方にOpen/Closeのコマンドを送ると、連動するようにSwitchBotアプリで設定していれば、両方同時に動きます。

わたしはひとまず、左側のカーテンのデバイスIDに対してコマンドを送ることにします。

これで、デバイスIDは取得できました。

続いて、コマンドを送ります。

## カーテン開閉のコマンド送信

引き続き、こちらの「温湿度計のデータを取得する」を参考にしてPythonプログラムを準備します。

https://qiita.com/taiki-kuraishi/items/7739cffc79611d27045c#%E6%B8%A9%E6%B9%BF%E5%BA%A6%E8%A8%88%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B

<details><summary>カーテン開閉のコマンド送信</summary>

```python
import os
import time
import json
import hashlib
import hmac
import base64
import uuid
import requests
import json
import datetime
from os.path import exists
from dotenv import load_dotenv

load_dotenv()

dir_name = "./responses"
if not exists(dir_name):
    os.makedirs(dir_name)

device_id = os.environ["SWITCHBOT_CURTAIN3_LEFT"]
print(f"https://api.switch-bot.com/v1.1/devices/{device_id}/commands")

token = os.environ["SWITCHBOT_TOKEN"]
secret = os.environ["SWITCHBOT_SECRET"]
nonce = str(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 = {}
apiHeader["Authorization"] = token
apiHeader["Content-Type"] = "application/json"
apiHeader["charset"] = "utf8"
apiHeader["t"] = str(t)
apiHeader["sign"] = str(sign, "utf-8")
apiHeader["nonce"] = nonce

data = {
    "commandType": "command",
    "command": "turnOn",
    # "command": "turnOff",
    "parameter": "default",
}

response = requests.post(
    f"https://api.switch-bot.com/v1.1/devices/{device_id}/commands",
    headers=apiHeader,
    json=data,
)

devices = response.json()

timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
response_file = f"{dir_name}/status_{device_id}_{timestamp}.json"
with open(response_file, "w") as f:
    json.dump(devices, f)

print("End.")
```
</details>

トークンや秘密鍵等セキュリティ情報の準備は、デバイスの一覧を取得したときと同様です。

その後、SwitchBot APIのドキュメントを参考にして、コマンド送信用のデータを準備し、POSTします。

これでPythonプログラム単体を動かすと、すごく長いラグの後、カーテンが動きます。

:::note
このラグは、以前はもう少し短かった気がしますが、今この記事を書くための再検証をしていると、けっこう長かったです。
SwitchBot APIが混み合っているか、クラウドとの距離で時間がかかっているのかわかりませんが、長いですね。
:::

# バッチファイル

Stream Deckのプログラムを動かすデフォルトは、バッチファイルです。

```bat
@echo off
python D:\PythonProgram\CurtainOpen.py
```

ネットで調べて、こんな感じで書きました。

ひとまずダブルクリックして実行したところ、カーテンが動いたため、Pythonプログラムは正常に実行されています。

# Stream Deckの「システム」カテゴリーの「開く」

後は、ここにバッチファイルを設定するだけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/40d5b592-5691-0924-fe1c-6ebfb004fccc.png)

# 終わりに

これにより、Stream Deckをぽちっとすることで、カーテンが開閉するようになりました。

応用して（デバイスIDを書き換えれば）赤外線リモコンを制御している電灯のON/OFFもできるようになります。

現時点では、ラグがすごいため、あまり実用性はありませんが、遊んでみるのは楽しいものです。

少なくとも、オンラインゲーム中に部屋が暗くなってきたから、カーテン閉めて電灯をつける、みたいなことが座ったままできるようになったのは、多少感動するのではないでしょうか。

みなさんも、トークンや秘密鍵などのセキュリティ情報の管理にはじゅうぶんに気をつけて、遊んでみてはいかがでしょうか。

# 202412031700追記
ためしに、家のルーター設定で、ダイナミックDNSになっていたところを、GoogleとCloudflareに向けたスタティックに変えてみたところ、ラグが軽くなりました。