概要
M5PaperからSwitchBotを操作するために、2022年9月に新しくなったSwitchBotAPI v1.1に挑戦しました。
公式のリクエストを作成するサンプルには、C++が存在しなかったため苦労しました。
API呼び出しまでの流れ
- ココのとおりアプリで開発者設定を行う
- アプリから トークン 、シークレットコードを取得する
- curlなどをつかって操作したいデバイスのdeviceIDを取得する
curl --request GET 'https://api.switch-bot.com/v1.0/devices' -H 'Authorization: <じぶんのとーくん>' -H 'Content-Type: application/json; charset=utf8'
- 開発環境でAPIをリクエストする処理を実装する
苦労したところ
v1.1になったことで認証が厳しくなり、リクエストのヘッダーを作るのに苦労しました。
- UNIXエポック時刻(1970年からのミリ秒)
.cpp
time_t tmTmp;
time(&tmTmp);
unsigned long long tmTmpL = (unsigned long long)tmTmp * 1000;
String tmStr = String(tmTmpL);
- UUID生成
.cpp
uint8_t uuid[16];
ESPRandom::uuid4(uuid);
String uuidStr = ESPRandom::uuidToString(uuid);
- HMAC(SHA-256)生成
.cpp
char hmac_result[45];
String data = String(PRM_SWBOT_AUTH_CODE) + tmStr + uuidStr;
generateHMAC(PRM_SWBOT_SECRET_KEY, data.c_str(), hmac_result);
.cpp
void generateHMAC(const char* key, const char* message, char* output) {
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
const mbedtls_md_info_t* md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
mbedtls_md_setup(&ctx, md_info, 1);
mbedtls_md_hmac_starts(&ctx, (const unsigned char*)key, strlen(key));
mbedtls_md_hmac_update(&ctx, (const unsigned char*)message, strlen(message));
unsigned char hash[32];
mbedtls_md_hmac_finish(&ctx, hash);
mbedtls_md_free(&ctx);
size_t outlen;
mbedtls_base64_encode(NULL, 0, &outlen, hash, sizeof(hash));
mbedtls_base64_encode((unsigned char*)output, outlen+1, &outlen, hash, sizeof(hash));
output[outlen] = '\0';
}
クラス化
使いやすいようにクラスにしました。
また、コールバック関数から呼び出したいのでシングルトンになっています。
Control_Switchbot.cpp
#include "control_switchbot.h"
#include "params_switchbot.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <mbedtls/md.h>
#include <mbedtls/base64.h>
#include <Arduino.h>
#include <ESPRandom.h>
#include <esp_log.h>
#include <RTClib.h>
Control_Switchbot* Control_Switchbot::instance = nullptr;
Control_Switchbot* Control_Switchbot::getInstance()
{
if (instance == nullptr)
{
instance = new Control_Switchbot();
}
return instance;
}
Control_Switchbot::Control_Switchbot()
{
// 時刻合わせ
configTime(3600*9, 0, "pool.ntp.org");
}
Control_Switchbot::~Control_Switchbot()
{
// none
}
int Control_Switchbot::pushSwitchBot(String dvcID, String *respons)
{
return postRequest(dvcID, "/commands", "{\"commandType\":\"command\",\"command\":\"press\"}", respons);
}
void Control_Switchbot::generateHMAC(const char* key, const char* message, char* output) {
mbedtls_md_context_t ctx;
mbedtls_md_init(&ctx);
const mbedtls_md_info_t* md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
mbedtls_md_setup(&ctx, md_info, 1);
mbedtls_md_hmac_starts(&ctx, (const unsigned char*)key, strlen(key));
mbedtls_md_hmac_update(&ctx, (const unsigned char*)message, strlen(message));
unsigned char hash[32];
mbedtls_md_hmac_finish(&ctx, hash);
mbedtls_md_free(&ctx);
size_t outlen;
mbedtls_base64_encode(NULL, 0, &outlen, hash, sizeof(hash));
mbedtls_base64_encode((unsigned char*)output, outlen+1, &outlen, hash, sizeof(hash));
output[outlen] = '\0';
}
int Control_Switchbot::requestDeviceList(String *respons)
{
return postRequest("", "", "", respons);
}
int Control_Switchbot::requestStatus(String dvcID, String *respons)
{
return postRequest(dvcID, "/status", "", respons);
}
int Control_Switchbot::postRequest(String dvcID, String cmd, String body, String *respons)
{
*respons = "";
if (!WiFi.isConnected())
{
ESP_LOGI("main", "wifi connect fail");
return 0;
}
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
ESP_LOGI("main", "get time fail");
return 0;
}
time_t tmTmp;
time(&tmTmp);
unsigned long long tmTmpL = (unsigned long long)tmTmp * 1000;
String tmStr = String(tmTmpL);
ESP_LOGI("main", "tmStr:%s\n", tmStr.c_str());
uint8_t uuid[16];
ESPRandom::uuid4(uuid);
String uuidStr = ESPRandom::uuidToString(uuid);
char hmac_result[45];
String data = String(PRM_SWBOT_AUTH_CODE) + tmStr + uuidStr;
generateHMAC(PRM_SWBOT_SECRET_KEY, data.c_str(), hmac_result);
ESP_LOGI("main", "uuid:%s\n", uuidStr.c_str());
HTTPClient http;
String add = "https://api.switch-bot.com/v1.1/devices/" + dvcID + cmd;
http.begin(add);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", String(PRM_SWBOT_AUTH_CODE));
http.addHeader("sign", hmac_result);
http.addHeader("nonce", uuidStr);
http.addHeader("t", tmStr);
http.POST(body);
*respons = http.getString();
ESP_LOGI("main", "responsels:%s\n", respons->c_str());
return 1;
}
control_switchbot.h
#ifndef _CONTROL_SWITCHBOT_H_
#define _CONTROL_SWITCHBOT_H_
#include <M5EPD.h>
#include "params_switchbot.h"
class Control_Switchbot {
private:
static Control_Switchbot* instance;
Control_Switchbot();
public:
static Control_Switchbot* getInstance();
~Control_Switchbot();
int pushSwitchBot(String dvcID, String *respons);
private:
void generateHMAC(const char* key, const char* message, char* output);
int requestDeviceList(String *respons);
int requestStatus(String dvcID, String *respons);
int postRequest(String dvcID, String cmd, String body, String *respons);
};
#endif //_CONTROL_SWITCHBOT_H_
穴埋めの部分は、取得したトークンやID等を使用してください。
params_switchbot.h
#ifndef _PARAMS_SWITCHBOT_H_
#define _PARAMS_SWITCHBOT_H_
#define PRM_SWBOT_DEVICE_ID "hoge"
#define PRM_SWBOT_AUTH_CODE "xxxxxxxxxx"
#define PRM_SWBOT_SECRET_KEY "yyyyyyyyy"
#endif //_PARAMS_SWITCHBOT_H_
呼び出しの例
*.cpp
String res;
Control_Switchbot::getInstance()->pushSwitchBot(PRM_SWBOT_DEVICE_ID, &res);
さいごに
今回はボタンタイプのロボットを使いましたが、Bodyの部分を変更することで他の種類のボットにも対応できるはずです。
HMACの暗号化とか全然理解していなかったので苦労しました。ダメな時に帰ってくる”400 : Bad Request”は何が悪いか教えてほしい。
ChatGPTさんがとても頼りになりました!
M5Paperの開発は別記事にしました。
バグがあったら教えてください。