SwitchBotをPCから操作するスクリプトを作成するExcelツール【API v1.1対応版】

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です。

うまく動作しない場合は

commandTypeCommandcommand parameter
commandturnOndefault

はすべての機器に適用できる(たぶん)ので、この設定で電源を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関数でハッシュした文字列です。

$tGet-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のエンドポイントにリクエストを送信します。

ツールを入手する

利用上のご注意

  • ダウンロードしたファイルを利用したことにより生じた結果については、利用者ご自身に責任を負っていただきます。
  • ご利用前に使用方法をご確認ください。
  • 当方は成果物の正確性について最善を尽くしますが保証はいたしません。
  • Windows11 Microsoft365 環境でのみ動作確認済み。

Downloadボタンを押下した時点で注意事項に同意したものとみなします。

switchbot_api_command_generator_v11.zip

まとめ

PCの前から一切動かない生活が取り戻せて良かったです。

はじめてPowerShellスクリプトを作成してみましたが、当然ながら全体的にVBSより洗練されていていいですね。モダンな機能も標準モジュールで持っているので呼び出すだけで使えて簡単です。ダブルクリックで実行させない制約をMicrosoftが改めてくれれば、もっと普及すると思うのですが。

Pythonが実行できる環境があることが最強ですが、ダメな場合は今後はVBSよりPowerShellをまず検討したいです。

おわり。