C++言語でSwitchBot API v1.1 リクエスト

# 概要
[M5Paper](http://docs.m5stack.com/en/core/m5paper)からSwitchBotを操作するために、2022年9月に新しくなった[SwitchBotAPI v1.1](https://blog.switchbot.jp/announcement/api-v1-1/)に挑戦しました。
[公式](https://github.com/OpenWonderLabs/SwitchBotAPI)のリクエストを作成するサンプルには、C++が存在しなかったため苦労しました。


# API呼び出しまでの流れ
- [**ココ**](https://blog.switchbot.jp/announcement/api-v1-1/)のとおりアプリで開発者設定を行う
- アプリから **トークン** 、**シークレットコード**を取得する
- 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の開発は[**別記事**](https://qiita.com/akitaro7/items/bf8155dd5c6c30a7645a)にしました。

バグがあったら教えてください。