ESP32 (arduino) からAlexaの定型アクションをトリガーしよう

# 2023/10追記
今からやるのであれば、Matter対応版のほうがレスポンスも早く、クラウドに依存しないのでおすすめです。

https://qiita.com/wanko_in_lunch/items/70ec071083d3c5172880

追記終わり

# Alexaの定型アクションを音声操作以外から実行したい
そう思ったことありませんか？
例えば、洗濯機の完了をAlexaから教えてもらいたい、壁スイッチ的なもので複数メーカーのスマート機器を一括で操作したいなどなど

音声以外で定形アクションを実行する方法はいくつかあります。
日本未発売ですが、Amazon謹製Echo buttonを使う方法、Broadlink SR3を使う方法etc...

https://japan.cnet.com/article/35128591/

https://www.broadlink.ae/product-page/broadlink-sr3-4key-smart-button

Echo Buttonは公式なのでどうにでもなるとして、SR3は抜け道を使っています。
実はドアセンサー、動体センサーの検知を定型アクションのトリガーにすることが出来て、
SR3は見た目こそボタンですが、Alexaからは動体センサーとして認識されます。
つまり「ボタンを押した」を「動体センサーが検知した」に割り当てて、Alexaに送っているわけです。

# というわけでAlexaから認識されるドアセンサーをarduinoで作ろう
材料としてXIAO ESP32C3を使います。
安くてType-Cで何よりコンパクトなので最近のお気に入りです。

https://www.marutsu.co.jp/pc/i/2336957/

Alexaに認識してもらうプラットフォームとしてEspressif公式のESP Rainmakerというものを使用します。
ホビーユーザーであれば無料で利用可能ですが、中華サービスに不安があるようであれば自前でAWSスキル構築するのも良いと思います。

https://rainmaker.espressif.com/

# コード
動けばOKな雑魚プログラマーなのでコードの良し悪しは気にしない(ハードル下げ
RainMakerのサンプルをベースにぐっちゃぐちゃです。
特にデバイスの作成あたり酷い感はあるので修正案大歓迎です。

```c++:RMaker_Alexa_Routine_ButtonX4.ino
#include "RMaker.h"
#include "WiFi.h"
#include "WiFiProv.h"
bool wifiLowLevelInit(bool persistent);

#define NAME_DEVICE_BUTTON_1 "Button1"
#define NAME_DEVICE_BUTTON_2 "Button2"
#define NAME_DEVICE_BUTTON_3 "Button3"
#define NAME_DEVICE_BUTTON_4 "Button4"

#define NAME_PARAM_STATE "ButtonState"

const char *service_name = "PROV_ALEXA_ROUTINE_BUTTON";
const char *pop = "abcd1234";

//GPIO for push button
#if CONFIG_IDF_TARGET_ESP32C3
static int gpio_0 = 9;
static int gpio_switch = 7;
#else
//GPIO for virtual device
static int gpio_0 = 0;
static int gpio_switch = 16;
#endif

esp_rmaker_device_t*  device_Button1;
esp_rmaker_device_t*  device_Button2;
esp_rmaker_device_t*  device_Button3;
esp_rmaker_device_t*  device_Button4;

esp_rmaker_param_t*   Button1_param;
esp_rmaker_param_t*   Button2_param;
esp_rmaker_param_t*   Button3_param;
esp_rmaker_param_t*   Button4_param;

bool Button1_state = false;
bool Button2_state = false;
bool Button3_state = false;
bool Button4_state = false;



void sysProvEvent(arduino_event_t *sys_event)
{
    switch (sys_event->event_id) {      
        case ARDUINO_EVENT_PROV_START:
#if CONFIG_IDF_TARGET_ESP32S2
        Serial.printf("\nProvisioning Started with name \"%s\" and PoP \"%s\" on SoftAP\n", service_name, pop);
        printQR(service_name, pop, "softap");
#else
        Serial.printf("\nProvisioning Started with name \"%s\" and PoP \"%s\" on BLE\n", service_name, pop);
        printQR(service_name, pop, "ble");
#endif         
        break;
        default:;
    }
}

//Call back for contact sensor
static esp_err_t write_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx)
{
  //デバイス名とパラメータ名を取得 
  const char *device_name = esp_rmaker_device_get_name(device);
  const char *param_name = esp_rmaker_param_get_name(param);

  //どのデバイスが反応したか、どのパラメータが変更されたかで条件分け
  if(strcmp(device_name, NAME_DEVICE_BUTTON_1) == 0) 
  {
    if(strcmp(param_name, NAME_PARAM_STATE) == 0)
    {
      Serial.printf("Received value = %s for %s - %s\n", val.val.b? "true" : "false", device_name, param_name);

      //RainMakerへ変更値を送信
      esp_rmaker_param_update_and_report(param,val);

      //内部値も更新
      Button1_state = val.val.b;
    }
  }
  else if(strcmp(device_name, NAME_DEVICE_BUTTON_2) == 0) 
  {
    if(strcmp(param_name, NAME_PARAM_STATE) == 0)
    {
      Serial.printf("Received value = %s for %s - %s\n", val.val.b? "true" : "false", device_name, param_name);

      //RainMakerへ変更値を送信
      esp_rmaker_param_update_and_report(param,val);

      //内部値も更新
      Button2_state = val.val.b;
    }
  }
  else if(strcmp(device_name, NAME_DEVICE_BUTTON_3) == 0) 
  {
    if(strcmp(param_name, NAME_PARAM_STATE) == 0)
    {
      Serial.printf("Received value = %s for %s - %s\n", val.val.b? "true" : "false", device_name, param_name);

      //RainMakerへ変更値を送信
      esp_rmaker_param_update_and_report(param,val);

      //内部値も更新
      Button3_state = val.val.b;
    }
  }
  else if(strcmp(device_name, NAME_DEVICE_BUTTON_4) == 0) 
  {
    if(strcmp(param_name, NAME_PARAM_STATE) == 0)
    {
      Serial.printf("Received value = %s for %s - %s\n", val.val.b? "true" : "false", device_name, param_name);

      //RainMakerへ変更値を送信
      esp_rmaker_param_update_and_report(param,val);

      //内部値も更新
      Button4_state = val.val.b;
    }
  }
  

  return ESP_OK;
}


void setup()
{
  Serial.begin(115200);

  //ノード初期化前にWi-Fiの初期化が必要
  wifiLowLevelInit(true);

  //ノード作成&初期化
  esp_rmaker_config_t rainmaker_cfg =
  {
    .enable_time_sync = false,
  };
  esp_rmaker_node_t *node = esp_rmaker_node_init(&rainmaker_cfg, "Alexa_Routine_Device", "ESP RainMaker with Arduino");

  if (!node)
  {
    Serial.printf("Node Initialize Error!\n");
    vTaskDelay(5000/portTICK_PERIOD_MS);
    abort();
  }

  //デバイス作成
  device_Button1 = esp_rmaker_device_create(NAME_DEVICE_BUTTON_1, "esp.device.contact-sensor", NULL);
  if(device_Button1)
  {
    //名前要素の追加　注意：名前を2バイト文字に設定するとAlexaアプリから認識されてないので注意
    esp_rmaker_device_add_param(device_Button1,  esp_rmaker_name_param_create("Name", NAME_DEVICE_BUTTON_1));

    //接触センサとしてデバイスを登録するためのパラメータを作成
    Button1_param = esp_rmaker_param_create(NAME_PARAM_STATE, "esp.param.contact-detection-state", esp_rmaker_bool(Button1_state), PROP_FLAG_READ | PROP_FLAG_WRITE);

    if(Button1_param)
    {
      esp_rmaker_param_add_ui_type(Button1_param, "esp.ui.toggle"); //Rainmakerアプリから見た時のUIを追加
      esp_rmaker_device_add_param(device_Button1, Button1_param); //デバイスにパラメータを紐づけ
      esp_rmaker_device_add_cb(device_Button1, write_cb, NULL); //デバイスにコールバックを紐づけ
      esp_rmaker_node_add_device(node, device_Button1); //ノードにデバイスを追加
    }
  }

  //2~4個目も同様に作成
  device_Button2 = esp_rmaker_device_create(NAME_DEVICE_BUTTON_2, "esp.device.contact-sensor", NULL);
  if(device_Button2)
  {
    esp_rmaker_device_add_param(device_Button2,  esp_rmaker_name_param_create("Name", NAME_DEVICE_BUTTON_2));

    Button2_param = esp_rmaker_param_create(NAME_PARAM_STATE, "esp.param.contact-detection-state", esp_rmaker_bool(Button2_state), PROP_FLAG_READ | PROP_FLAG_WRITE);

    if(Button2_param)
    {
      esp_rmaker_param_add_ui_type(Button2_param, "esp.ui.toggle");
      esp_rmaker_device_add_param(device_Button2, Button2_param);
      esp_rmaker_device_add_cb(device_Button2, write_cb, NULL);
      esp_rmaker_node_add_device(node, device_Button2);
    }
  }

  device_Button3 = esp_rmaker_device_create(NAME_DEVICE_BUTTON_3, "esp.device.contact-sensor", NULL);
  if(device_Button3)
  {
    esp_rmaker_device_add_param(device_Button3,  esp_rmaker_name_param_create("Name", NAME_DEVICE_BUTTON_3));

    Button3_param = esp_rmaker_param_create(NAME_PARAM_STATE, "esp.param.contact-detection-state", esp_rmaker_bool(Button3_state), PROP_FLAG_READ | PROP_FLAG_WRITE);

    if(Button3_param)
    {
      esp_rmaker_param_add_ui_type(Button3_param, "esp.ui.toggle");
      esp_rmaker_device_add_param(device_Button3, Button3_param);
      esp_rmaker_device_add_cb(device_Button3, write_cb, NULL);
      esp_rmaker_node_add_device(node, device_Button3);
    }
  }

  device_Button4 = esp_rmaker_device_create(NAME_DEVICE_BUTTON_4, "esp.device.contact-sensor", NULL);
  if(device_Button4)
  {
    esp_rmaker_device_add_param(device_Button4,  esp_rmaker_name_param_create("Name", NAME_DEVICE_BUTTON_4));

    Button4_param = esp_rmaker_param_create(NAME_PARAM_STATE, "esp.param.contact-detection-state", esp_rmaker_bool(Button4_state), PROP_FLAG_READ | PROP_FLAG_WRITE);

    if(Button4_param)
    {
      esp_rmaker_param_add_ui_type(Button4_param, "esp.ui.toggle");
      esp_rmaker_device_add_param(device_Button4, Button4_param);
      esp_rmaker_device_add_cb(device_Button4, write_cb, NULL);
      esp_rmaker_node_add_device(node, device_Button4);
    }
  }

  //OTA機能有効 Wi-Fi経由でスケッチ書き換えできるのかもしれないが使い方不明
  esp_rmaker_ota_enable_default();

  //RainMakerスタート
  RMaker.start();

  WiFi.onEvent(sysProvEvent);
#if CONFIG_IDF_TARGET_ESP32S2
  WiFiProv.beginProvision(WIFI_PROV_SCHEME_SOFTAP, WIFI_PROV_SCHEME_HANDLER_NONE, WIFI_PROV_SECURITY_1, pop, service_name);
#else
  WiFiProv.beginProvision(WIFI_PROV_SCHEME_BLE, WIFI_PROV_SCHEME_HANDLER_FREE_BTDM, WIFI_PROV_SECURITY_1, pop, service_name);
#endif
}

void loop()
{
  if(digitalRead(gpio_0) == LOW)
  { //GPIO0読み取り

    delay(100);
    int startTime = millis();
    while(digitalRead(gpio_0) == LOW) delay(50);
    int endTime = millis();
    if ((endTime - startTime) > 10000) 
  
    {
    // 10秒以上押された場合、設定完全初期化
      Serial.printf("Reset to factory.\n");
      RMakerFactoryReset(2);
    }
    else if((endTime - startTime) > 3000)
    {
      // 3~10秒押された場合、Wi-Fi初期化
      Serial.printf("Reset Wi-Fi.\n");
      RMakerWiFiReset(2);
    }
    else
    {
      // ボタン1状態反転
      Button1_state = !Button1_state;
      Serial.printf("Button1 State to %s.\n", Button1_state ? "true" : "false");

      //スケッチ側からデバイス状態を変えるには↓の関数を呼ぶ
      esp_rmaker_param_update_and_report(Button1_param,esp_rmaker_bool(Button1_state));
    }
  }

  // シリアルや他のセンサー、スイッチなどから叩く例
  if(Serial.available() > 0)
  {
    //受信バッファがなくなるまでシリアル通信読み取り
    while (Serial.available() > 0) 
    {
      Serial.read();
    }

    Serial.println("Button2 State ON via Serial");

    Button2_state = true;
    //スケッチ側からデバイス状態を変えるには↓の関数を呼ぶ
    esp_rmaker_param_update_and_report(Button2_param,esp_rmaker_bool(Button2_state));
  }

  //ボタン3,4も同様
  
  delay(100);
}
```

コンパイル時に、Partition SchemeをRainMakerに設定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188873/3e10f753-1e56-4114-aa22-281f79289461.png)

# Rainmakerスマホアプリ、Alexaスキル登録
AppStoreでRainmakerと検索し、アプリのインストールとアカウントの作成を行ってください。
またAlexaアプリからRainmakerスキルのインストール、作成したアカウントでのログインを行います。
上手くいかなければ、RainmakerアプリのSettingsタブ→Voice services→Amazon Alexaからアカウントリンクも試してみてください。
このあたりは一般的なスマート機器メーカーの登録手順と変わらないかと思います。

# Rainmakerにデバイス登録
RainmakerアプリのDevicesタブの右上の＋からデバイスの追加を行います。
QRコードを読む方法もありますが、面倒なので「I don't have a QR Code」でOKです。
選択肢はBLEを選びます。(ESP32 S2であればSoftAPを選ぶとのこと)

周辺のBLEデバイス検索が始まるのでコード頭の方で設定した
`char *service_name = "PROV_ALEXA_ROUTINE_BUTTON";`
のデバイス名が表示されているはずです。
デバイス選択後、
`char *pop = "abcd1234";`
で指定したパスワードを打ち込みペアリングします。

その後、接続先のWi-FiのSSID、PWを教えてあげます。
このあたりも一般的なスマート機器メーカーの登録と同様かと思います。

DevicesタブにAlexa_Routine_Deviceが表示されればOK！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188873/9fbda8c4-4f72-b43b-0af6-ff9eeeae33a5.png)


# Alexaアプリからデバイスの検出、定型アクションに登録
Rainmakerアプリでデバイス登録した瞬間にAlexaアプリからの通知で新しいデバイスが検出された旨が表示されます。
検出されない、部分的に検出されない場合、「デバイスを追加」→「その他」→「Wi-Fi」あたりを選んでおけば検索してくれます。

試しに定型アクションを作成し、実行条件に「Button1が開いた時」を指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188873/4b6d43ee-3763-2d44-d5a7-68c27f62f26a.png)

BOOTボタン(GPIO0)を短押ししてAlexaが喋ってくれれば完成です！
(コードを見るとわかりますが長押しすると初期化されるので気をつけてください)

# 思うままに定型アクションをトリガーしよう！
上記コードではシリアル通信でなにか文字を受信するとButton2が開になるようになっています。
3,4は実装していませんが同様に操作することが出来ます。
GPIOに繋いだタクトスイッチから実行するもよし、電流センサーで消費電力を測ってトリガーするもよし、
なんでもいけるでしょう。

# 注意点、ハマりポイント
* **RainMakerアプリからデバイス検索したときに作ったESP32デバイスが出て来ない**
新品のESP32ではなく別のプロジェクトで使用していたものを流用すると、Wi-Fi接続情報が下手に残っているせいで検索待ちモードに入れないときがあります。
BOOTボタン(GPIO0)を10秒間押すと完全初期化できるので、まずはこれを試してみてください。
また、BLEで使ってた/デバイス登録の途中で失敗した場合、スマホのBluetoothペアリング済みデバイスに登録が残っているせいでうまく出てきません。
この場合はOSの設定アプリからペアリングを解除してください。
<br>

* **Alexaからデバイスが認識されない**
esp_rmaker_device_create()やesp_rmaker_param_create()の引数に2バイト文字を使用すると、RainMakerアプリからは認識するのに、Alexaアプリからは認識されません。
必ず英数字で名前付けしてあげてください。
なお、Alexaアプリから認識できた後、Alexaアプリ上でのデバイス名を日本語にすることは可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188873/9cc49adb-2a91-48e1-8fe2-163dea4205ff.png)

* **センサー開→定型アクション実行までの反応が遅い**
そういう仕様のようです。
壁スイッチのように押してすぐ照明が点くのをイメージすると、ちょっとラグが大きいですね。