ESP32(M5 ATOM S3 Lite)でSwitchBot Web APIを操作する mbedTLSを使って認証する
本日も、SwitchBot推し活としてWeb APIのネタでお届けします。
今日は、ESP32のシステム(M5Stack ATOM S3 Liteという製品)を使います。
M5Stack ATOM S3 LiteはESP32プラットフォームの小型のガジェットで、単体でWiFiやBLE(Bluetooth Low Energy)が利用できます。電源はUSB Type-Cで供給。内部で動作するプログラムはVS Code + Platform IOを使ってC++ (Arduino互換)で書けます。極小筐体ですが、GPIOピンにもすぐにアクセスできるし、押しボタンやLEDランプも搭載されているので、ちょっとした電子工作にとても重宝する機器です。
SwitchBot連携にもってこいの製品で、筆者のお気に入りでもあります。
それでは、いってみましょう!
SwitchBotの「シーン」について
訳あって、SwitchBotの「シーン」をM5の小さな機器から呼び出すことを考えました。
SwitchBotの「シーン」は、SwitchBotアプリで扱う抽象的な概念で、スイッチのオフ・オン操作などを複数組み合わせて、一つのコマンドのように実行できる実行単位のことをいいます。
例えば、電源プラグとテープライトをいっぺんに「オン」にしたい場合、「全体をオンする」というシーンを作って、「そのシーンの実行」を行うと、電源プラグもテープライトもいっぺんにオンできる、という感じで使います。
「シーン」はSwitchBotアプリで作る(設定する)ことができ、様々なSwitchBot商品群の機器を操作可能。開閉センサーをトリガーにして作った「シーン」を実行できるなど、組み合わせ次第でさまざまなホームオートメーションを実現でる仕組みです。
Web APIで「シーン」の実行も可能
さて、このブログ「テック大家さん」で何度かお伝えしているように、SwitchBotの魅力はAPIが公開されていることです。
エンジニアがAPIを使ってプログラミングすることで究極のスマートホームを作れてしまうわけです。ということは、SwitchBotのみならず、他のスマートデバイスや他のクラウドシステムなどなどを組み合わせて、アイディア次第で想いおもいのシステムを実現できます。
そう、マニア垂涎の状況なのです。
公開されているAPIは、クラウドベースのWeb API と BLE(Bluetooth Low Energy) APIの2種類。
それぞれでできることは微妙に異なりますが、用途に応じて使い分けできるというのは非常にありがたいです。エンジニアとしてはそれだけで気分が上がりますね(笑)。
2つの公開APIのうちクラウドベースのWeb APIでは、あらかじめSwitchBotアプリで作成した「シーン」を操作できます。公開APIを組み合わせれば「シーン」相当の機能を実現できるのはご想像の通り。それでも、API一発たたけば、リモートから自動化したコマンド群(シーン)を手軽に実行できるのは便利です。
SwitchBotが公開しているAPI仕様やオープンソースの状況もまとめていますので、ご興味あればそちらも御覧ください。
今回の記事で扱いたいのは、SwitchBot Web APIをESP32(M5Stack)で呼び出してシーンを実行してしまおう、ということなのです。
ところで、筆者はこれまでも本ブログでESP32のプラットフォームでBLEを使って直接SwitchBot機器を操作する技術を解説しました。以下の記事に詳細はゆずりますので、SwitchBotのBLE APIにご興味あればこちらも併せてご覧下さい↓
ESP32 (M5Stack)でWeb APIを使うときの課題
大抵どこのシステムでもWeb APIを使うとなれば「認証」がついて回ります。SwtichBotもご多分に漏れずAPI呼び出しの際に認証情報が必要になります。
筆者は以前の記事で、SwitchBotのWeb APIでどんなことができるのかを大雑把に解説しています(下記リンク参照)。この中で認証の話題も出てきてます。SwitchBot Web APIの認証の場合、Web APIを呼び出す前提としてSwitchBotアプリからtokenとsecretという2つの文字列をあらかじめ取得しておきます。以下の説明はtoken/secretを取得しているものとして進めます。
APIを呼び出す際には、このtoken/secretを組み合わせてHTTPヘッダに含めるいくつかのパラメータを構築しなければなりません。
ありがたいことにこの手順は、SwitchBot APIの公式ドキュメントに詳しく書かれています。特に、Web系でよく使用されるプログラミング言語ではドキュメントにサンプルコードがあり、ほとんどコピペでことが済みます。
例えば、筆者が愛用するJavaScriptの場合は以下のように記述します(公式ドキュメントより転載、一部省略)
const crypto = require('crypto');
const https = require('https');
const token = "yourToken";
const secret = "yourSecret";
const t = Date.now();
const nonce = "requestID";
const data = token + t + nonce;
const signTerm = crypto.createHmac('sha256', secret)
.update(Buffer.from(data, 'utf-8'))
.digest();
const sign = signTerm.toString("base64");
console.log(sign);
const body = JSON.stringify({
"command": "turnOn",
"parameter": "default",
"commandType": "command"
});
const deviceId = "MAC";
const options = {
hostname: 'api.switch-bot.com',
port: 443,
path: `/v1.1/devices/${deviceId}/commands`,
method: 'POST',
headers: {
"Authorization": token,
"sign": sign,
"nonce": nonce,
"t": t,
'Content-Type': 'application/json',
'Content-Length': body.length,
},
};
const req = https.request(options, res => {
console.log(`statusCode: ${res.statusCode}`);
res.on('data', d => {
process.stdout.write(d);
});
});
ただ、今回のようなESP32(Platform IOを使ってArduinoで実装する)の場合、上記のような他の言語用に書かれた手順をC++のコードにしなければならないわけです。
いくつか課題と解決策を見ていきましょう。
認証情報の暗号化
上記のJavaScriptのコードを分析するとわかりますが、SHA 256の暗号化が必要になります。
JavaScriptではcryptoというライブラリが利用できますが、ESP32の場合はどうでしょう?
生成AIのClaudeさんに相談したところ、ESP32プラットフォームでは、mbedTLSという暗号用のライブラリが搭載されており、これを使えます。
SwitchBot APIのJavaScriptサンプルでは、最初にsignTermというダイジェストを生成し、それをBase64でエンコードした文字列に変換しています。
ESP32の場合は以下のようなコードでこれを実現できます。
// HMAC-SHA256署名の生成関数
static String generateSignature(const String& token, const String& secret, const String& timestamp, const String& nonce) {
// 署名文字列の作成: token + timestamp + nonce
String signaturePayload = token + timestamp + nonce;
// HMACキーの設定
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1);
// HMAC-SHA256ダイジェストの計算
uint8_t hmacResult[32];
mbedtls_md_hmac_starts(&ctx, (const unsigned char*)secret.c_str(), secret.length());
mbedtls_md_hmac_update(&ctx, (const unsigned char*)signaturePayload.c_str(), signaturePayload.length());
mbedtls_md_hmac_finish(&ctx, hmacResult);
// ダイジェストをBase64エンコード
String signature = base64Encode(hmacResult, 32);
// メモリの解放
mbedtls_md_free(&ctx);
return signature;
}
Base64エンコード
mbedTLSライブラリの一部には、Base64エンコードの関数(mbedtls_base64_encode)もあります。ダイジェストの数値の配列をBase64文字列に変換するのにこれが使えます。
// Base64エンコード関数
static String base64Encode(const uint8_t* data, size_t length) {
size_t encodedLength = 0;
// まず必要なバッファサイズを計算
mbedtls_base64_encode(NULL, 0, &encodedLength, data, length);
// バッファを確保
uint8_t* encodedBuffer = (uint8_t*)malloc(encodedLength + 1);
encodedBuffer[encodedLength] = '\0';
// エンコード実行
size_t actualLength = 0;
mbedtls_base64_encode(encodedBuffer, encodedLength + 1, &actualLength, data, length);
// 文字列に変換
String result = String((char*)encodedBuffer);
// メモリ解放
free(encodedBuffer);
return result;
}
現在時刻の取得
上のJavaScriptのプログラムにあるように、HTTPヘッダに"t"というパラメータを含める必要があります。
この値は、JavaScriptのDate.now()の値、つまり、1970年からの経過時間をミリ秒で表したものです。ということはシステムが現在時刻を知っていなければならないわけです。M5Stackでは現在時刻がそのままだとわからないので、NTPでネットから取得することにしました。
arduino-libraries/NTPClient@^3.2.1 というNTPのクライアントライブラリを使っています。以下のように初期化します。
// NTP Client
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 60000);
さらに、setup()でtimeClient.begin()を呼び出し、loop()でtimeClient.update()を呼び出しておきます。
その上で、以下のようなコードで1970年からの経過時間を取り出すことが可能になります。
// タイムスタンプの取得(ミリ秒単位)
unsigned long long epoch = timeClient.getEpochTime();
unsigned long long currentTime = epoch * 1000ULL;
String timestamp = String(currentTime);
ESP32でWeb API経由でシーンを実行
ここまでできれば、Web APIコール時に必要となるHTTPヘッダの情報をすべて埋めることができます。あとはAPI仕様に応じて、必要なパラメータをボディに乗せ、HTTPSでURLを適切に叩けばいいわけです。
シーンを実行するためには、HTTPボディに含めるのは以下のようなJSONです。シーンのIDはSwitchBotアプリでシーンを作成したときに一意に振られる文字列です。あらかじめ別のAPIで取得しておきます。
{"sceneId": "ssss-cccc-eeee-nnnn-eeee"}
APIのエントリーポイントは、https://api.switch-bot.com/v1.1/scenes/{sceneId}/executeといった感じになりますので、全体のコードは以下のようになります。
boolean CallSwitchbot::executeScene(const String& sceneId) {
// タイムスタンプの取得(ミリ秒単位)
unsigned long long epoch = timeClient.getEpochTime();
unsigned long long currentTime = epoch * 1000ULL;
String timestamp = String(currentTime);
// String timestamp = String(millis());
// ノンス(リクエストID)の生成
String nonce = "ESP32_" + String(random(10000));
// 署名の生成
String signature = generateSignature(API_TOKEN, API_SECRET, timestamp, nonce);
// JSONペイロードの作成
String jsonPayload = "{\"sceneId\":\"" + sceneId + "\"}";
// HTTPクライアントの設定
HTTPClient http;
String url = BASE_URL + "/scenes/" + sceneId + "/execute";
http.begin(url);
// HTTPヘッダーの設定
http.addHeader("Authorization", API_TOKEN);
http.addHeader("sign", signature);
http.addHeader("nonce", nonce);
http.addHeader("t", timestamp);
http.addHeader("Content-Type", "application/json");
http.addHeader("Content-Length", String(jsonPayload.length()));
// POSTリクエストの送信
int httpResponseCode = http.POST(jsonPayload);
// レスポンスの処理
bool success = false;
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println("Response: " + response);
success = true;
} else {
Serial.println("Error sending command");
}
http.end();
return success;
}
ソースコード
全体のソースコードは、筆者のDIYエンターテインメントシステムの一部に組み込んでいますので、そのrepositoryのリンクを張っておきます。
ディスカッション
コメント一覧
まだ、コメントがありません