Googleアシ子とSwitchBotにより、声で大半の機器が操作ができるようになった我が家ですが、日本引きこもり能力検定1級免許所持(自称)の私は「OK、Google」という発声すら面倒になり、Googleアシ子とのコミュニケーションを拒絶し、PCからクリックするだけで家中の機器をコントロールすることを画策したのでした。それがこちら。
これですべてが順調にいき、引きこもり検定特級に昇級間近のように思われましたが、ある日突然、PCから指令を出してもSwitchBotがうんともすんとも言わなくなりました。
原因はどうやらSwitchBot APIのバージョンアップがあったからのようです。公式サイトでは旧バージョンのAPIでの使用も継続できると説明されていましたが、私の環境ではシーンが実行できなくなりました。
API v1.0 → v1.1変更点
バージョンアップで何が変わったかというと、セキュリティが強化されました。具体的にはリクエストヘッダーに持たせる情報が増えました。あとエンドポイントも変わりました。
詳細は公式サイトをご確認いただくとして、以前のツールではVBSでリクエストを送信していましたが、新バージョンではHMACで署名を生成しなければならず、これがVBSには荷が重すぎました。やろうと思えばできないことはない感じですが、そこまでやるんだったら素直にPythonをインストールしたほうがよくね?レベルです。
よって今回はPowerShellスクリプトを利用することにしました。前からわかっていたことですが、おとなしくPythonでやった方がいいことには触れない約束です。
システム概要
しくみは単純で、APIに必要なパラメーターを付けたHTTPリクエストを送信するPowerShellスクリプトをExcel-VBAでメタプログラミングで出力して、それを実行しています。
PowerShellスクリプトはダブルクリックから実行できないので、batファイル経由で実行するようにしています。そのbatファイルもVBAからメタプログラミングしています。
Excelツールを配布しますので皆さんもお試しください。
使用方法
SwitchBotのスマホアプリから機器が操作可能な状態になっている必要があります。
1.SwitchBot APIを利用するためのトークンとクライアントシークレットを発行します。
スマホアプリのメニューからプロフィール → 設定 → アプリバージョンを10回くらいタップすると新たに開発者向けオプションが追加されるのでタップします。トークンを取得をタップするとトークンが表示されます。コピーをタップしてテキストファイルなどに保存しておきます。同様にクライアントシークレットも保存しておきます。
これらが他人に知られると、勝手に照明がついたり、TVのチャンネルが変わったりといったポルターガイスト現象の原因となるので、絶対に漏洩しないように厳重に管理してください。
2.このページのダウンロードボタンからExcelファイルを入手します。
ダウンロードしたzipファイルを展開してください。
3.展開したExcelファイルの「認証」シートに1.でコピーしたトークン、クライアントシークレットを貼り付けます。
uuid4は署名処理に使用します。毎回ランダムな文字列が表示されます。ワークシート関数で作成していますので、消さないようにご注意ください。インターネッツで拾った関数なのでもし消してしまったらGoogle先生で見つけてください。
4.「機器リスト」シートの機器リスト取得ボタンを押します。
あなたがSwitchBotで操作している機器が表示されます。
5.「コマンドジェネレーター」シートに操作したい機器のdeviceIdをコピペします。
6.deviceTypeによって指定できる操作が下へ一覧表示されるので参考にしてcommandType、Command、command parameterをそれぞれ入力します。
一覧表示はFilter関数を使用しているのでMicrosoft365をご利用の方のみ表示されます。表示できない場合は「commandlist」シートを参照するかSwitchBotAPIのページで確認してください。もしくはMicrosoftへ上納金を納めてください。
設定例をいつくか載せておくので参考にしてください。
設定例 Botの押す操作を実行
設定例 TVを1chにする
設定例 エアコンを25℃、冷房、風量自動でONにする
7.設定値を入力した状態でテストボタンを押すと、APIをコールします。
結果がtest resultに表示されます。実際に機器が動作すればOKです。
うまく動作しない場合は
commandType | Command | command parameter |
command | turnOn | default |
はすべての機器に適用できる(たぶん)ので、この設定で電源をONにすることができるか確認してください。
8.PS出力ボタンを押します。
Excelツールと同じフォルダにbatフォルダとpssフォルダが作成されます。
それぞれにファイルが出力されます。
9.batフォルダの.batファイルを開くとAPIがコールされ操作が実行されます。
実際にはpssフォルダのPowerShellスクリプトが実行されています。実行時に一瞬コマンドプロンプトの黒い画面が表示されますが仕様です。消す方法もあるようですが、ツール側では対応しません。ご自身で実装してください。
PowerShellスクリプトにはトークン、クライアントシークレットがそのまま書き込まれているので第三者に公開しないようにご注意ください。
batフォルダからショートカットをデスクトップなどに作って、アイコンをそれらしくすると、なお良いでしょう。私が作成したなんちゃってSwitchBotアイコンも付属していますので、ご利用ください。
シーンを実行したい場合は「シーン」シートのシーンリスト取得ボタンでシーン一覧を取得して対象のシーンのIDをsceneIDセルに入力します。
シーンのテストとファイル出力については、前述の方法に準じます。自分でコマンドを組んでいくのが面倒な機器はスマホアプリでシーンにしてしまって、シーンを実行したほうが簡単かもしれません。
SwitchBot APIのコール数の上限は1万回/日です。家電の操作だけであれば超過する方が難しいのではないかと思われますので、好きなだけ使ってください。
解説:VBAソースコード
SwitchBot APIの要求する署名はトークンと送信時刻(UNIXタイムスタンプ:13桁)とUUIDを連結した文字列を、クライアントシークレットをキーとしてSHA256関数でハッシュした文字列です。これを生成する処理が主な変更点です。
VBAでのAPIリクエストはテスト時だけ利用しますのでさほど重要ではありません。キモはPowerShellスクリプトの方なのでさらっといきます。VBAで何をしているかは旧バージョンのツールの解説に詳しく書いてあります。旧バージョンとの違いはAuthモジュールの存在くらいです。
Mainモジュール。
Option Explicit
Const HOST_DOMAIN = "https://api.switch-bot.com"
Const API_VERSION = "v1.1"
Enum deviceList
deviceID = 1
deviceName
deviceType
End Enum
Enum sceneList
sceneID = 1
sceneName
End Enum
Function createHttp(method As String, url As String) As Object
With Worksheets("認証")
Dim token As String
Dim secret As String
Dim nonce As String
token = .Range("b1").Value
secret = .Range("b2").Value
nonce = .Range("b3").Value
End With
Dim t As String
t = UnixTimestamp(Now()) * 1000
Dim http As Object
Set http = CreateObject("MSXML2.XMLHTTP")
Dim sign As String
sign = Base64HMAC("SHA256", token & t & nonce, secret)
With http
.Open method, url, False
.setRequestHeader "Authorization", token
.setRequestHeader "t", t
.setRequestHeader "sign", sign
.setRequestHeader "nonce", nonce
.setRequestHeader "Content-Type", "application/json; charset=utf8"
End With
Set createHttp = http
End Function
Sub getSwitchbotDeviceList()
Dim http As Object
Set http = createHttp("GET", HOST_DOMAIN & "/" & API_VERSION & "/devices")
http.send
Do While http.readyState < 4
DoEvents
Loop
Dim rb As Object
Set rb = JsonConverter.ParseJson(http.responseText)("body")
With Worksheets("機器リスト")
Dim i As Long
i = 2
Dim dl
Dim il
'SwitchBot製品
For Each dl In rb("deviceList")
.Cells(i, deviceList.deviceID).Value = dl("deviceId")
.Cells(i, deviceList.deviceName).Value = dl("deviceName")
.Cells(i, deviceList.deviceType).Value = dl("deviceType")
i = i + 1
Next
'HubでIr制御している機器
For Each il In rb("infraredRemoteList")
.Cells(i, deviceList.deviceID).Value = il("deviceId")
.Cells(i, deviceList.deviceName).Value = il("deviceName")
.Cells(i, deviceList.deviceType).Value = il("remoteType")
i = i + 1
Next
End With
End Sub
Sub execCommand()
Dim deviceID As String
Dim commandType As String
Dim command As String
Dim commandParam As String
deviceID = Range("a2").Value
commandType = Range("b2").Value
command = Range("c2").Value
commandParam = Range("d2").Value
Range("h1").Value = ""
Range("h2").Value = ""
Dim http As Object
Set http = createHttp _
("POST", HOST_DOMAIN & "/" & API_VERSION & "/devices/" & deviceID & "/commands")
Dim body As String
body = "{""command"": """ & command & """, ""parameter"": """ & _
commandParam & """, ""commandType"": """ & commandType & """}"
http.send body
Dim rt As Object
Set rt = JsonConverter.ParseJson(http.responseText)
Range("h1").Value = rt("statusCode")
Range("h2").Value = rt("message")
End Sub
Sub getSwitchbotSceneList()
Dim http As Object
Set http = createHttp("GET", HOST_DOMAIN & "/" & API_VERSION & "/scenes")
http.send
Do While http.readyState < 4
DoEvents
Loop
Dim rb As Object
Set rb = JsonConverter.ParseJson(http.responseText)("body")
With Worksheets("シーン")
Dim i As Long
i = 2
Dim sl
For Each sl In rb
.Cells(i, sceneList.sceneID).Value = sl("sceneId")
.Cells(i, sceneList.sceneName).Value = sl("sceneName")
i = i + 1
Next
End With
End Sub
Sub execScene()
Dim sceneID As String
sceneID = Range("f2").Value
Range("k1").Value = ""
Range("k2").Value = ""
Dim http As Object
Set http = createHttp _
("POST", HOST_DOMAIN & "/" & API_VERSION & "/scenes/" & sceneID & "/execute")
http.send
Dim rt As Object
Set rt = JsonConverter.ParseJson(http.responseText)
Range("k1").Value = rt("statusCode")
Range("k2").Value = rt("message")
End Sub
Sub commandPS()
Call exportPss("command")
End Sub
Sub scenePS()
Call exportPss("scene")
End Sub
Authモジュール。HMAC関連。コードにコメントで入れているWEBサイトから拝借しました。
Option Explicit
Function UnixTimestamp(dt As Date) As Variant
UnixTimestamp = DateDiff("s", "1/1/1970 09:00:00", dt)
End Function
Public Function Base64HMAC(ByVal sType As String, ByVal sTextToHash As String, ByVal sSharedSecretKey As String)
'https://excelbaby.com/learn/excel-macro-base64-hmac-encryption/
Dim asc As Object, enc As Object
Dim TextToHash() As Byte
Dim SharedSecretKey() As Byte
Set asc = CreateObject("System.Text.UTF8Encoding")
sType = UCase(sType)
Select Case sType
Case "SHA1"
Set enc = CreateObject("System.Security.Cryptography.HMACSHA1")
Case "SHA256"
Set enc = CreateObject("System.Security.Cryptography.HMACSHA256")
Case "SHA384"
Set enc = CreateObject("System.Security.Cryptography.HMACSHA384")
Case "SHA512"
Set enc = CreateObject("System.Security.Cryptography.HMACSHA512")
Case Else
Base64HMAC = "Error! sType value: SHA1, SHA256, SHA384, SHA512"
Exit Function
End Select
TextToHash = asc.Getbytes_4(sTextToHash)
SharedSecretKey = asc.Getbytes_4(sSharedSecretKey)
enc.Key = SharedSecretKey
Dim bytes() As Byte
bytes = enc.ComputeHash_2((TextToHash))
Base64HMAC = EncodeBase64(bytes)
Set asc = Nothing
Set enc = Nothing
End Function
Private Function EncodeBase64(ByRef arrData() As Byte) As String
'https://excelbaby.com/learn/excel-macro-base64-hmac-encryption/
Dim objXML As Object
Set objXML = CreateObject("Msxml2.DOMDocument.6.0")
Dim objNode As Object
Set objNode = objXML.createElement("b64")
objNode.DataType = "bin.base64"
objNode.nodeTypedValue = arrData
EncodeBase64 = objNode.Text
Set objNode = Nothing
Set objXML = Nothing
End Function
Exportモジュール。
Option Explicit
Sub exportPss(mode As String)
Dim name As String
Dim er As Long
Dim col As Long
Select Case mode
Case "command"
With Worksheets("コマンドジェネレーター")
name = .Range("b4").Value & "_" _
& .Range("c2").Value
End With
er = 27
col = 1
Case "scene"
name = Worksheets("シーン").Range("f4").Value
er = 19
col = 2
End Select
Dim dp As String
dp = ThisWorkbook.Path & "\pss\"
Call makeDirIfNotExist(dp)
Dim pssPath As String
pssPath = dp & mode & "_" & name & ".ps1"
Open pssPath For Output As #1
Dim i As Long
For i = 1 To er
Print #1, Worksheets("pscode").Cells(i, col).Value
Next
Close #1
Call exportBat(mode, name, pssPath)
MsgBox "PowerShellスクリプトとして出力しました。", vbInformation + vbOKOnly, "完了"
End Sub
Sub exportBat(mode As String, name As String, pssPath As String)
Dim dp As String
dp = ThisWorkbook.Path & "\bat\"
Call makeDirIfNotExist(dp)
Open dp & mode & "_" & name & ".bat" For Output As #1
Print #1, "@echo off"
Print #1, "powershell -ExecutionPolicy RemoteSigned -WindowStyle Hidden -File """ & pssPath & """"
Print #1, "exit"
Close #1
End Sub
Sub makeDirIfNotExist(dp)
If Dir(dp, vbDirectory) = "" Then
MkDir dp
End If
End Sub
この他にJson処理にVBA-JSONモジュールを利用しています。
解説:PowerShellスクリプト
PowerShellスクリプトのコードはpscodeシートにあらかじめ入力してあり、トークンなどを各シートから参照して埋め込んで作成しています。それをExportのプロシージャでファイルへ書き出しています。
PowerShellスクリプトは今回はじめて触るので、Bing AIにコーディングをお願いしたら8割方書いてくれました。なので記法をなんとなくでしかわかっていませんが、うごくので良し。
コマンド実行のソースコードです。
$token = "token"
$secret = "client secret"
$deviceId = "device id"
$commandType = "command"
$command = "turnOn"
$commandParam = "default"
$uri = "https://api.switch-bot.com/v1.1/devices/${deviceId}/commands"
$t = Get-Date -UFormat %s
$t = [int]$t * 1000
$nonce = New-Guid
$message = [Text.Encoding]::UTF8.GetBytes($token + $t + $nonce)
$hmacsha = New-Object System.Security.Cryptography.HMACSHA256
$hmacsha.key = [Text.Encoding]::UTF8.GetBytes($secret)
$signature = $hmacsha.ComputeHash($message)
$signature_base64 = [Convert]::ToBase64String($signature)
$headers = @{
charset = "utf8"
Authorization = $token
sign = $signature_base64
t = $t
nonce = $nonce
}
$body = @{
"command" = $command
"parameter" = $commandParam
"commandType" = $commandType
} | ConvertTo-Json
Invoke-WebRequest -Uri $uri -Method Post -Body $body -ContentType "application/json" -Headers $headers
シーン実行のソースコードです。
$token = "token"
$secret = "client secret"
$sceneId = "scene id"
$uri = "https://api.switch-bot.com/v1.1/scenes/${sceneId}/execute"
$t = Get-Date -UFormat %s
$t = [int]$t * 1000
$nonce = New-Guid
$message = [Text.Encoding]::UTF8.GetBytes($token + $t + $nonce)
$hmacsha = New-Object System.Security.Cryptography.HMACSHA256
$hmacsha.key = [Text.Encoding]::UTF8.GetBytes($secret)
$signature = $hmacsha.ComputeHash($message)
$signature_base64 = [Convert]::ToBase64String($signature)
$headers = @{
charset = "utf8"
Authorization = $token
sign = $signature_base64
t = $t
nonce = $nonce
}
Invoke-WebRequest -Uri $uri -Method Post -ContentType "application/json" -Headers $headers
繰り返しになりますが、SwitchBot APIの要求する署名はトークンと送信時刻(UNIXタイムスタンプ:13桁)とUUIDを連結した文字列を、クライアントシークレットをキーとしてSHA256関数でハッシュした文字列です。
$tはGet-Date -UFormat %sで現在時刻をUNIXタイムスタンプで取得します。これにはmsecまでは含まれておらず10桁なので、$t = [int]$t * 1000で13桁に増やしています。
Get-Date -UFormatの戻りは文字列なので、文字列連結$t + "000"でもいい気がしますが、これだと何故か小数点が入ってバグるときがあるんですよね(よくわかってないだけ疑惑)。
UUIDはNew-Guidのたったひとつのステートメントだけで取得できます。便利。VBSだと死ぬほど面倒な処理が必要。
それらをつなげて、$message変数へ入れておきます。バイト列として。
$hmacsha変数へSystem.Security.Cryptography.HMACSHA256オブジェクトを格納します。keyプロパティにクライアントシークレットを設定します。バイト列として。
$hmacsha.ComputeHashメソッドに$messageを渡してハッシュ値を取得します。それをBase64エンコードして$signature_base64とします。これを2行でできちゃうのがスゴいです。VBSだと死ぬほど(略)。
$headersハッシュテーブルに前段で作成したもろもろを使用してリクエストヘッダーを組み立てます。
Invoke-WebRequestでAPIのエンドポイントにリクエストを送信します。
ツールを入手する
まとめ
PCの前から一切動かない生活が取り戻せて良かったです。
はじめてPowerShellスクリプトを作成してみましたが、当然ながら全体的にVBSより洗練されていていいですね。モダンな機能も標準モジュールで持っているので呼び出すだけで使えて簡単です。ダブルクリックで実行させない制約をMicrosoftが改めてくれれば、もっと普及すると思うのですが。
Pythonが実行できる環境があることが最強ですが、ダメな場合は今後はVBSよりPowerShellをまず検討したいです。
おわり。