よもやま話β版

よもやま話を書きます。内容はぺらぺら。自由に書く。

SwitchBotAPI を使う・再(v1.1)

よくAPIを見ていたらv1.1とあったので、v1.1でちゃんとやりたいなと思ってリトライ。 最新は公式を読んでください。 https://github.com/OpenWonderLabs/SwitchBotAPI

アプリを更新

SwitchBotのアプリがまず古すぎたので(V4.5.0だった…すみませんズボラで…)、ちゃんと更新。2023年10月時点でiOSアプリはV7.7。更新したら登録してた設定が全部消えたと思って一瞬絶望したが、ログアウトしてただけだった。ログインしたらちゃんと復活した。

リクエストに必要な値を準備する

APIがv1.1になったことにより、リクエストのために必要な値が追加で増えたので、準備していく。

tokenとsecret

アプリの 設定 > 開発者向けオプション を10回連打で出てくる「開発者向けオプション」に、「トークン」と「クライアントシークレット」の2種類の文字列が表示されるようになった。トークンはアプリ更新前後で変更がなかったが、クライアントシークレットを今回新しくゲットした。

timestamp(t)

13桁のタイムスタンプが必要とのこと。これはUNIX時間(ミリ秒)のこと。
GitHubのサンプルにある t = 1661927531000 は、UNIX時間の解釈でいうと 2022/08/31 15:32:11(JST) となる。
参考: https://ja.wikipedia.org/wiki/UNIX時間

nonce

nonceとは何かをちょっとよくわかってない状態から開始。ランダムに生成するワンタイムの暗号トークンのことを指し、自由に決めてよいものと解釈した。

signを生成

公式APIを参考にsignを生成する。自分はRubyが好きなのでRubyで試みる。以下たくさん参考。無事取れた!

require 'securerandom'
require 'base64'
require 'httpclient'

TOKEN = 'xxxxx...'
SECRET = 'xxxxx...'

t = (Time.now.to_i * 1000).to_s
nonce = SecureRandom.uuid
sign = OpenSSL::HMAC.digest('sha256', SECRET, TOKEN + t + nonce)

header = {
  'Authorization' => TOKEN,
  'sign' => Base64.strict_encode64(sign),
  't' => t,
  'nonce' => nonce,
}

client = HTTPClient.new
res = client.get('https://api.switch-bot.com/v1.1/devices', header: header)
puts res.body

#=> {"statusCode":100,"body":{"deviceList":[{"deviceId":"D7B...","deviceName":"ハブミニ AD","deviceType":"Hub Mini","hubDeviceId":"...."},...] }, "message": "success"}

余談: SwitchBotサーバ側、Headerのパラメータが先頭大文字だと受け入れてくれなかった件

当初、Rubyの標準ライブラリ net/http で実装していたが、うまく返事を得られなかった。

uri = URI.parse('https://api.switch-bot.com/v1.1/devices')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

req = Net::HTTP::Get.new(uri)
req['Authorization'] = token
req['sign'] = Base64.strict_encode64(sign)
req['t'] = t
req['nonce'] = nonce

res = http.request(req)
puts res.body
#=> {"message":"Unauthorized"}

Rubyで生成したsignを、サンプルにあるJavaScriptのコードに適用すると使える…という状況だったためかなり頭を抱えていたが、調べた結果、次のようなことがわかった。

  • net/http では、ヘッダーフィールド名の先頭を大文字に変換する。
  • SwitchBot API では、ヘッダーフィールド名を先頭大文字にしたもの ( Sign, T, Nonce ) は受け付けていない(らしい)。
    • ゆえに、net/http を使って SwitchBot API を叩いても受け付けてくれない。
  • おまけの気付き: 1度認証すると数十秒くらいは Authorization (token) しか見ずに返事をくれている気がする。
    • jsサンプルコードで 200 を得てから Rubyの動かないコードを動かすと 同じ200 をしばらく返してくれる。

今回は問題解決のために net/http の利用を諦めて、httpclient を使ったところ、無事リクエストを成功させることができた。

参考:

デバッグの様子(net/http)

# net/httpの様子をチェック

uri = URI.parse('https://api.switch-bot.com/v1.1/devices')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.set_debug_output $stderr # 標準出力に出す

req = Net::HTTP::Get.new(uri)
# …略…

opening connection to api.switch-bot.com:443...
opened
starting SSL for api.switch-bot.com:443...
SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES128-GCM-SHA256
<- "GET /v1.1/devices HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api.switch-bot.com\r\nAuthorization: xxxxx...\r\nSign: xxxxx...\r\nT: 1696770306000\r\nNonce: 34523524-e0a1-406d-bb20-f2dfcc2f0155\r\nConnection: close\r\n\r\n"
-> "HTTP/1.1 401 Unauthorized\r\n"
-> "Date: Sun, 08 Oct 2023 13:05:07 GMT\r\n"
-> "Content-Type: application/json\r\n"
-> "Content-Length: 26\r\n"
-> "Connection: close\r\n"
-> "x-amzn-RequestId: ae44ece8-bd88-41fd-8842-2c12caafa200\r\n"
-> "x-amzn-ErrorType: UnauthorizedException\r\n"
-> "x-amz-apigw-id: Me9YoEy3oAMEeIg=\r\n"
-> "\r\n"
reading 26 bytes...
-> "{\"message\":\"Unauthorized\"}"
read 26 bytes
Conn close

デバッグの様子(httpclient)

# httpclientの様子をチェック
client = HTTPClient.new
client.debug_dev = STDOUT # 標準出力に出す
res = client.get('https://api.switch-bot.com/v1.1/devices', header: header)
puts res.body

= Request

! CONNECT TO api.switch-bot.com:443
! CONNECTION ESTABLISHED
GET /v1.1/devices HTTP/1.1
Authorization: xxxxx...
sign: xxxxx...
t: 1696770516000
nonce: 90c09bc6-08b1-4078-b560-241a4c428e87
User-Agent: HTTPClient/1.0 (2.8.3, ruby 3.2.1 (2023-02-08))
Accept: */*
Date: Sun, 08 Oct 2023 13:08:36 GMT
Host: api.switch-bot.com



= Response

HTTP/1.1 200 OK
Date: Sun, 08 Oct 2023 13:08:37 GMT
Content-Type: application/json
Content-Length: 543
Connection: keep-alive
x-amzn-RequestId: 32b88a02-9b34-4a76-a4d4-73916e2ca52b
x-amz-apigw-id: Me95VGk7IAMEh5Q=
X-Amzn-Trace-Id: Root=1-6522a9d5-491163743186f65919874a15;Sampled=0;lineage=c8c2b0f2:0|bf95bacf:0