SwitchBot HubをESP32で自作(1) -SwitchBotをAlexaで操作する-
新年に立てた目標の1つ 週に1つは記事を書くという目標がいきなり頓挫するのもマズいので、1月は身の回りの利便化のため、SwitchBot HubをESP32で自作するという内容で記事を書いていく予定です。
ターゲットと具体的プランの検討
我が家にはAmazonのEchoやEcho Dotがすでに設置してある環境となっていますが、そのうちの1部屋(寝室)では設置してあるだけで、音楽用にしか使用できていないため、もう少し部屋のIoT化による利便化を図りたいと思います。
まずは具体的なターゲットを確認して、必要な機能の洗い出しと対応方法を検討します。
ターゲット
- 部屋の照明(壁面スイッチ)をAlexaで操作したい
- 部屋の家電をAlexaで操作したい
- 部屋の温湿度を測定・表示したい
- 外観を損ねない(基盤むき出しとか、配線がゴチャつくとかはNG)
- できるだけ安く整えたい <- ここ重要
必要な機能
- 物理スイッチの操作
- 家庭内Lan(Wifi)接続によるAlexa接続
- 赤外線による家電操作(赤外線の受信、送信、コマンド登録)
- 温湿度センサーの搭載
- 表示ディスプレイの搭載
対応方法
ターゲットの1~4を達成するだけであれば、SwitchBotのセットを買って一式そろえるのが一番手っ取り早いと思います。
が、SwitchBotとHub, 温度計と3点セットでAmazonのセール時に買っても1万円近くするため、肝心のできるだけ安くというところが厳しくなります。
なので、出来るだけ安く達成するため、デバイスの自作を検討していきます。
物理スイッチの検討
物理スイッチの操作についてはSwitchBotの自作をされている方もいるようですが、
物理ボタンをIoT化 – ESP32で Swich bot を自作し、スマホからスイッチをON,OFFできるようにしてみた
電源で配線が必要だったり、モーターむき出しというのは避けたいので、バッテリーをつけたりモーターつけたりとすると、安価・小型・おしゃれの3つを自作で達成するのはかなりハードなので、物理スイッチについてはSwitchBotをおとなしく使います。(セール時に買ったので2980円) www.amazon.co.jp
Alexa接続の検討
SwitchBotはBLEしか搭載していないため、直でAlexaに接続できず、何かしらの家庭内ネットワークに接続できる中継器を挟む必要があります。
素直にいくなら、SwithBot Hubなどの別製品が必要なのですが、そこそこの値段がするため、BLEとWifiモジュール搭載で自作IoTといえばコレというくらい広く普及し安価なESP32を使って省コスト化します。 akizukidenshi.com
国内の代理店だと1000円強しますが、AliExplessなどで買うと300~400円で買えたりします。(但し技適に注意)
ESP32に - Wifi接続 - SwitchBotの操作(BLE) - Alexaデバイス化 の機能を持たせることで、Alexaと接続し、SwitchBotを操作することができそうです。
その他機能
赤外線による家電操作(赤外線の受信、送信、コマンド登録)
温湿度センサーの搭載
表示ディスプレイの搭載
これらについては
- 赤外線受信モジュール
- 赤外線LED
- 温度センサ
- 湿度センサ
- LCDディスプレイ をESP32に搭載してやったら最低限の機能は果たせるはず、という安易な考えで、次回以降に持ち越しとします。
SwitchBotをAlexaで操作する
先人の知恵
ESP32で同じようなことは多くの方がされているようですので、先人の知恵を コピペ参考に させて頂きます。
- AlexaからのESP32の操作
AmazonEchoのAlexaとESP32を使って声で照明の明るさを調節することができるか基礎実験をしてみました | kohacraftのblog
- SwithBotの操作
DSAS開発者の部屋:SwitchBot を ESP32 で遠隔操作してみた
開発環境
手順
大まかには下記の手順で作業をすればよいです。
細かいところは参考記事の記事等を参照していただくなりググってもらうとして、アレンジした点のみ記載しておきます。
ライブラリの追加
Arduinoのライブラリ管理からEsplexa
を追加しておきます。
ソースコードの作成
動作の構成としてはこんな感じになります。
- 初期化
- BLE初期化(ライブラリの初期化とBLE接続時の動作の登録)
- Wifiの接続
- Esplexaの初期化(デバイス名・呼び出し時のアクションの登録)
- ループ
- Esplexaで待ち受け
- Alexaからの呼び出し
- Esplexaの初期化で登録した関数の呼び出し(SwitchBotの操作)
実際のソースは文末に記載しています。
これを使う際には
- WifiのSSID/PW
- SwitchBotのMACアドレス
- SwitchBotの動作モード(ボタンorスイッチ)に合わせてSWITCH_BOT_MODE
の変更
- DeviceName
(Alexaに表示される名称)
を使用環境に合わせてください。
ESP32への書き込み
これが少しハマりました。
ソース通りビルドすると
text section exceeds available space in board最大1310720バイトのフラッシュメモリのうち、スケッチが1403338バイト(107%)を使っています。 最大327680バイトのRAMのうち、グローバル変数が51396バイト(15%)を使っていて、ローカル変数で276284バイト使うことができます。 スケッチが大きすぎます。http://www.arduino.cc/en/Guide/Troubleshooting#size には、小さくするコツが書いてあります。 ボードESP32 Dev Moduleに対するコンパイル時にエラーが発生しました。
とエラーが出てビルドできませんでした。
どうやら、BLEのライブラリがかなり容量を食うらしく、スケッチだけで1.4MBを超えているようです。
デフォルトだと、アプリケーションの最大容量が1.28MBの設定となっているようなので、Arduinoの設定から変更します。とりあえずNO OTAを選択しました。
BLEとWifiを使うだけで容量をかなり食うというのは今後モリモリ機能追加しようと思うと、この辺のアプリケーション容量がネックになってきそうです。
Alexaの設定
Alexaでデバイスの追加を行うと、DeviceName
が表示されるはずですので、これを追加するだけです。
今回は照明用のスイッチとしてSwitchBotをつけているので、照明デバイスとして追加します。
SwitchBotの設置
こんな感じで設置してます。
動作
Alexaへの呼びかけ
「Alexa,照明をつけて」、「Alexa, 照明を消して」
でSwitchBotが動作して、照明のON/OFFをAlexa経由でできました。
照明以外のデバイスにSwitchBotの取り付ける場合には、照明以外のデバイスとして登録して、独自の名称をつけることで変更できるかと思います。
ソースコード
ESP32_Alexa.ino
#include <WiFi.h> #include <Espalexa.h> #include "BLEDevice.h" // prototypes boolean connectWifi(); //callback functions void changeSwitch(uint8_t val); /////////////////Constant values/////////////////////// //Alexa const char* deviceName = "Bedroom light"; //wifi const char* ssid = "Buffalo-D-C6CE"; const char* password = "hk6f47sue6e76"; //Switch bot const char* switchBotMac = "F7:62:D9:3B:70:37"; static BLEUUID SERV_SWITCHBOT("cba20d00-224d-11e6-9fb8-0002a5d5c51b"); static BLEUUID CHAR_SWITCHBOT("cba20002-224d-11e6-9fb8-0002a5d5c51b"); //command of Switch bot #define SWITCH_BOT_MODE 1 //0 : Button, 1 : Switch #if SWITCH_BOT_MODE == 0 static uint8_t cmdPush[3] = {0x57, 0x01, 0x00}; //Push and Pull #else static uint8_t cmdPress[3] = {0x57, 0x01, 0x01}; //Turn On static uint8_t cmdPull[3] = {0x57, 0x01, 0x02}; //Turn Off #endif //SWITCH_BOT_MODE //Grobal val boolean wifiConnected = false; BLEScan* pBLEScan; static BLEAddress *pGattServerAddress; static BLEAdvertisedDevice* myDevice; static BLERemoteCharacteristic* pRemoteCharacteristic; static BLEClient* pClient = NULL; Espalexa espalexa; void setup() { int i = 0; Serial.begin(115200); InitializeBLE(); for (; i < 100; i++) { wifiConnected = connectWifi(); if(wifiConnected){ espalexa.addDevice(deviceName, changeSwitch,0); //simplest definition, default state off espalexa.begin(); break; } delay(3000); } if(!wifiConnected){ Serial.println("Cannot connect to WiFi. Please check data and reset the ESP."); esp_restart(); } } void loop() { espalexa.loop(); delay(100); } //callback functions void changeSwitch(uint8_t val) { Serial.print("Switch is changed to "); if (val) { Serial.print("ON"); Serial.print(val); } else { Serial.println("OFF"); } connectAndSendCommand(*pGattServerAddress ,val); } ///////////////////////////Wifi//////////////////////////// // connect to wifi – returns true if successful or false if not boolean connectWifi(){ boolean state = true; int i = 0; WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.println("Connecting to WiFi"); // Wait for connection Serial.print("Connecting..."); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); if (i > 20){ state = false; break; } i++; } Serial.println(""); if (state){ Serial.print("Connected to "); Serial.println(ssid); Serial.print("IP address: "); Serial.println(WiFi.localIP()); } else { Serial.println("Connection failed."); } return state; } ////////////////////////////BLE///////////////////////// // For debug class MyClientCallback : public BLEClientCallbacks { void onConnect(BLEClient* pclient) { Serial.println("onConnect"); } void onDisconnect(BLEClient* pclient) { Serial.println("onDisconnect"); } }; // アドバタイズ検出時のコールバック class advdCallback: public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { Serial.printf("Advertised Device: %s \n", advertisedDevice.toString().c_str()); if (advertisedDevice.haveServiceUUID()) { String addr = advertisedDevice.getAddress().toString().c_str(); Serial.printf("It have service addr: %s \n", advertisedDevice.getAddress().toString().c_str()); if (addr.equalsIgnoreCase(switchBotMac)) { Serial.println("Found BLE"); advertisedDevice.getScan()->stop(); pGattServerAddress = new BLEAddress(advertisedDevice.getAddress()); myDevice = new BLEAdvertisedDevice(advertisedDevice); } } } }; // SwitchBot の GATT サーバへ接続 ~ Press コマンド送信 static bool connectAndSendCommand(BLEAddress pAddress, uint8_t isTurnOn) { Serial.println("connectAndSendCommand Start!"); try { pClient = BLEDevice::createClient(); Serial.println(myDevice->getAddress().toString().c_str()); pClient->setClientCallbacks(new MyClientCallback()); Serial.println("Connecting BLE"); while (!pClient->connect(myDevice)) { Serial.println("reconnect"); delay(1000); } // 対象サービスを得る Serial.println("pRemoteService"); BLERemoteService* pRemoteService = pClient->getService(SERV_SWITCHBOT); if (pRemoteService == nullptr) { Serial.println("e:service not found"); return false; } // 対象キャラクタリスティックを得る Serial.println("pRemoteCharacteristic"); pRemoteCharacteristic = pRemoteService->getCharacteristic(CHAR_SWITCHBOT); if (pRemoteCharacteristic == nullptr) { Serial.println("e:characteristic not found"); return false; } // キャラクタリスティックに Press コマンドを書き込む #if SWITCH_BOT_MODE == 0 Serial.println("Send Push"); pRemoteCharacteristic->writeValue(cmdPush, sizeof(cmdPush), false); #else Serial.println("check turnOn/off"); if(isTurnOn){ pRemoteCharacteristic->writeValue(cmdPress, sizeof(cmdPress), false); Serial.println("Send Press"); }else{ pRemoteCharacteristic->writeValue(cmdPull, sizeof(cmdPull), false); Serial.println("Send Pull"); } #endif //SWITCH_BOT_MODE delay(500); //ここが長すぎると、アレクサからエラー(デバイスの応答がない)が出るので注意 pClient->disconnect(); pClient = NULL; } catch (...) { Serial.println("Error"); if (pClient) { pClient->disconnect(); pClient = NULL; } return false; } return true; } void InitializeBLE(){ // BLE 初期化 BLEDevice::init(""); // デバイスからのアドバタイズをスキャン pBLEScan = BLEDevice::getScan(); pBLEScan->setAdvertisedDeviceCallbacks(new advdCallback()); pBLEScan->setActiveScan(true); pBLEScan->start(5,false); }