Swichbot温湿度計とM5Stackシリーズでエアコンを自動制御する+Alexaに対応

M5Stackシリーズで赤外線リモコンを作り、Swichbot温湿度計の温度を取得してエアコン自動制御します

作ってから気づいたんですけどこれHubの機能そのままですね(´・ω・`)

##使うデバイス##

ATOM Lite - スイッチサイエンス
https://www.switch-science.com/catalog/6262/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/36c327e5-d7ec-02a5-02c9-322ed5be2dae.png)


M5Stackシリーズの一番安い奴です
968円ってESP32 Dev kitより安いんですけど？！

Groveと赤外線LEDが内蔵されてて赤外線リモコンを作るのにうってつけです

***

M5Stack用赤外線送受信ユニット - スイッチサイエンス
https://www.switch-science.com/catalog/5699/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/6f8e52a3-836e-bd7a-e6ab-f901cc279818.png)

Atom lite内蔵赤外線LEDは射程距離が短くてエアコンを操作できないのでこれを使います
GroveケーブルでM5Stackシリーズと接続するだけで使えます

Amazonなどで赤外線LEDやレシーバを売ってますが回路を組むのがめんどいのでUnit使った方がいいと思います
ケースに収まってるし安いし(308円)

***

Amazon.co.jp： SwitchBot スイッチボット デジタル 温湿度計 スマート家電 – 高精度 スイス製センサースマホで温度湿度管理 アラーム付き グラフ記録 Alexa, GoogleHome, IFTTT対応 (ハブ必要): DIY・工具・ガーデン
https://www.amazon.co.jp/dp/B07L4QNZVF/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/0b3fc08b-af62-5d6e-dea4-7a6d31e9f9dd.png)

BLEで外部デバイスと通信する機能を持った温湿度計です
AlexaやGoogle Homeと連携させるためにはハブが必要ですが
今回の用途では必要ありません

1,980円ですがたまにセールをやるようなので狙うのも良いかもしれません

***

##赤外線リモコンを構成する##

M5StickCで赤外線リモコンを作ろう - Qiita
https://qiita.com/coppercele/items/ed91646944ca28ff0c07

こちらの記事をご覧ください

##Swichbot温湿度計から温度を取得する##

こちらの記事を参考にして温度を取得します

SwitchBot温湿度計のデータをM5StickC(ESP32)+Arduinoで取得する - Qiita
https://qiita.com/takeru@github/items/f42381e8482c3bf484e7

そして温度を取得してその温度に応じて赤外線でコマンドを飛ばせばいいと思ったのですが

**なぜか動きません**
※ハマリポイントその１

```cpp
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
(略)
      float temperature = ( servicedata[3] & 0b00001111 ) / 10.0 + ( servicedata[4] & 0b01111111 );
      if(!isTemperatureAboveFreezing){
        temperature = -temperature;
      }
      int humidity = servicedata[5] & 0b01111111;

      printf("battery:     %d\n",   battery);
      printf("temperature: %.1f\n", temperature);
      printf("humidity:    %d\n",   humidity);
      printf("\n");
      if (27 < temperature) {
      M5.dis.drawpix(0, 0x0000f0);
        sendAc(true, 24); // エアコンが反応しない
        printState();
        M5.dis.drawpix(0, 0x000000);
        ready = false;
      }
      else if (temperature < 25) {
        M5.dis.drawpix(0, 0x0000f0);
        sendAc(true, 28); //  // エアコンが反応しない
        printState();
        M5.dis.drawpix(0, 0x000000);
        ready = false;
      }
    }
};

void sendAc(bool on, int temp) {
  mitsuAc.setPower(on); 
  mitsuAc.setFan(kMitsubishiHeavy152FanAuto );
  mitsuAc.setMode(kMitsubishiHeavyCool);
  mitsuAc.setTemp(temp);
  mitsuAc.setSwingVertical(kMitsubishiHeavy152SwingVLow );
  mitsuAc.setSwingHorizontal(kMitsubishiHeavy152SwingHAuto );
  mitsuAc.send();
}

```

BLEの仕様なのか、マルチスレッドにしてるせいなのかとかいろいろ試したのですが、
なぜかloop()の中ではエアコンが反応したのでそのようにしました

```cpp
float temperature = 26.0;
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
(略)
      temperature = ( servicedata[3] & 0b00001111 ) / 10.0 + ( servicedata[4] & 0b01111111 );
      if(!isTemperatureAboveFreezing){
        temperature = -temperature;
      }
      int humidity = servicedata[5] & 0b01111111;

      printf("battery:     %d\n",   battery);
      printf("temperature: %.1f\n", temperature);
      printf("humidity:    %d\n",   humidity);
      printf("\n");
      ready = true;
    }
};

void loop() {
  M5.update();
  if (ready) {
    if (27 < temperature) {
      M5.dis.drawpix(0, 0x0000f0);
      sendAc(true, 24);
      printState();
      M5.dis.drawpix(0, 0x000000);
      ready = false;
    }
    else if (temperature < 25) {
      M5.dis.drawpix(0, 0x0000f0);
      sendAc(true, 28);
      printState();
      M5.dis.drawpix(0, 0x000000);
      ready = false;
    }
  }
  delay(1);
}

```
動いたのはいいのですが結局理由が分からなかったので誰か検証してください・・・(´・ω・`)

PID制御とか入れようかな～と思ったんですけど多分エアコンの中の人がいい感じにやってくれるから要らないなって結論に達しました(´・ω・`)

##温度を取得する周期を設定する##

最初はscanTime変数がスキャンする周期を設定するのだと思っていたのですが
どうもソースを見る限りタイムアウト時間の設定のようです
※ハマリポイントその２

setup()中に

```cpp

  pBLEScan->setInterval(10000);
  pBLEScan->setWindow(9999);  // less or equal setInterval value

```

があったのでこれだ!と思ったんですけどBLEの仕様によって10秒(10000)以上は設定できないようです

なので結局BLEスキャンをマルチスレッドに出してdelay()を入れることにしました

```cpp
uint16_t scanInterval = 600000; //In ms

void Task1(void *pvParameters) {
  while(autoControl) {
    BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
    pBLEScan->clearResults();
    delay(scanInterval);
  }
  vTaskDelete(NULL);
}

void loop() {
  M5.update();
  if (M5.Btn.wasPressed()) {
    xTaskCreatePinnedToCore(Task1,"Task1", 4096, NULL, 3, NULL, 1);
  }

  delay(1);
}

```


##エアコン自動制御を構築する##

ハマリポイントを抜けたのでエアコン自動制御するスケッチを作ります

```cpp
#include "M5Atom.h"

#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <ir_MitsubishiHeavy.h>

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

int scanTime = 3; //In seconds
uint16_t scanInterval = 600000; // 10分(In ms)

BLEScan* pBLEScan;

BLEUUID serviceUUID     = BLEUUID("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
BLEUUID serviceDataUUID = BLEUUID("00000d00-0000-1000-8000-00805f9b34fb");

const uint16_t kIrLed = 12;   // Atom lite内蔵LED
const uint16_t acLed = 26; // IR Unit

IRsend irsend(kIrLed);
IRMitsubishiHeavy152Ac  mitsuAc(acLed);

const char *ssid = "";
const char *password = "";

bool autoControl = false;
bool ready = false;

void printState() {
  // Display the settings.
  Serial.println("Panasonic A/C remote is in the following state:");
  Serial.printf("  %s\n", mitsuAc.toString().c_str());
  // Display the encoded IR sequence.
  unsigned char* ir_code = mitsuAc.getRaw();
  Serial.print("IR Code: 0x");
  for (uint8_t i = 0; i < kPanasonicAcStateLength; i++)
    Serial.printf("%02X", ir_code[i]);
  Serial.println();
}

void sendAc(bool on, int temp) {
  mitsuAc.setPower(on); 
  mitsuAc.setFan(kMitsubishiHeavy152FanAuto );
  mitsuAc.setMode(kMitsubishiHeavyCool);
  mitsuAc.setTemp(temp);
  mitsuAc.setSwingVertical(kMitsubishiHeavy152SwingVLow );
  mitsuAc.setSwingHorizontal(kMitsubishiHeavy152SwingHAuto );
  mitsuAc.send();
}

float temperature = 26.0;
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {

      if(!advertisedDevice.haveServiceUUID()) return;
      if(!advertisedDevice.getServiceUUID().equals(serviceUUID)) return;
      printf("SwitchBot Meter!\n");

      if(!advertisedDevice.haveServiceData()) return;
      std::string s = advertisedDevice.getServiceData();

      if(!advertisedDevice.getServiceDataUUID().equals(serviceDataUUID)) return;

      const char* servicedata = s.c_str();
      int battery = servicedata[2] & 0b01111111;
      bool isTemperatureAboveFreezing = servicedata[4] & 0b10000000;
      temperature = ( servicedata[3] & 0b00001111 ) / 10.0 + ( servicedata[4] & 0b01111111 );
      if(!isTemperatureAboveFreezing){
        temperature = -temperature;
      }
      int humidity = servicedata[5] & 0b01111111;

      printf("battery:     %d\n",   battery);
      printf("temperature: %.1f\n", temperature);
      printf("humidity:    %d\n",   humidity);
      printf("\n");

    }
};

void Task1(void *pvParameters) {
  while(autoControl) {
    BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
    pBLEScan->clearResults();
    ready = true; // loop内で
    delay(scanInterval);
  }
  vTaskDelete(NULL);
}

void setup()
{
  Serial.begin(115200);
  M5.begin(true, false, true);

  irsend.begin();
  mitsuAc.begin();
  M5.begin();

  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), true);
  pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
  pBLEScan->setInterval(10000);
  pBLEScan->setWindow(9999);  // less or equal setInterval value
}

void loop()
{
  M5.update();
  if (M5.Btn.wasPressed())
  {
    Serial.println("Btn.wasPressed() == TRUE");
    autoControl = true;
    xTaskCreatePinnedToCore(Task1,"Task1", 4096, NULL, 3, NULL, 1);
  }
  if (ready) {
    if (27 < temperature) {
      M5.dis.drawpix(0, 0x0000f0);
      sendAc(true, 24);
      printState();
      M5.dis.drawpix(0, 0x000000);
      ready = false;
    }
    else if (temperature < 25) {
      M5.dis.drawpix(0, 0x0000f0);
      sendAc(true, 28);
      printState();
      M5.dis.drawpix(0, 0x000000);
      ready = false;
    }
  }

  delay(1);
}

```

この例では三菱エアコンで気温が28度より高かったら冷房を24度に強め、
25度を下回ったら冷房を28度にするように制御しています
これはお好みで変更してください
自動制御のログも載せておきます

```
Auto AC ON
SwitchBot Meter!
temperature: 27.4
Panasonic A/C  Power: On, Mode: 1 (Cool), Temp: 24C
27.3
27.2
27.1
26.9
(略)
25.0
24.9
Panasonic A/C   Power: On, Mode: 1 (Cool), Temp: 28C
24.8
24.9
25.1
25.2
```

##Alexaに対応する##

EspAlexaを導入するとAlexaで操作することができます
詳しくはこちらをご覧ください

M5StickC赤外線リモコンをAlexaに対応させよう - Qiita
https://qiita.com/coppercele/items/8c962dabadf33d52709b


ただしBLEとEspAlexaを同時に使用するとスケッチが大きくなりすぎるので
パーテションの設定を変更して対応します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/298776/7863cca2-4d5e-7a8f-a81e-6815b22be614.png)


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">NoOTAﾔｯﾀｰ! <a href="https://t.co/0yHicae9IO">pic.twitter.com/0yHicae9IO</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1276496741462716416?ref_src=twsrc%5Etfw">June 26, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<details><summary>EspalexaSwichbot.ino</summary>
<div>

```cpp

#include <WiFi.h>
#include "M5Atom.h"
#include <Espalexa.h>

#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <ir_MitsubishiHeavy.h>

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>

int scanTime = 3; //In seconds
uint16_t scanInterval = 600000; // 10分(In ms)

BLEScan* pBLEScan;

BLEUUID serviceUUID     = BLEUUID("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
BLEUUID serviceDataUUID = BLEUUID("00000d00-0000-1000-8000-00805f9b34fb");

const uint16_t kIrLed = 12;   // ESP8266 GPIO pin to use. Recommended: 4 (D2).
const uint16_t acLed = 26; // ESP8266 GPIO pin to use. Recommended: 4 (D2).

IRsend irsend(kIrLed); // Set the GPIO to be used to sending the message.
IRMitsubishiHeavy152Ac  mitsuAc(acLed);
// Change this!!
const char *ssid = "";
const char *password = "";

// prototypes
bool connectWifi();

//callback functions
void terebiChanged(EspalexaDevice *dev);
void airconChanged(EspalexaDevice *dev);
void zidouChanged(EspalexaDevice *dev);

bool wifiConnected = false;

Espalexa espalexa;

bool autoControl = false;
bool ready = false;

void printState() {
  // Display the settings.
  Serial.println("Panasonic A/C remote is in the following state:");
  Serial.printf("  %s\n", mitsuAc.toString().c_str());
  // Display the encoded IR sequence.
  unsigned char* ir_code = mitsuAc.getRaw();
  Serial.print("IR Code: 0x");
  for (uint8_t i = 0; i < kPanasonicAcStateLength; i++)
    Serial.printf("%02X", ir_code[i]);
  Serial.println();
}

void sendAc(bool on, int temp) {
  mitsuAc.setPower(on); 
  mitsuAc.setFan(kMitsubishiHeavy152FanAuto );
  mitsuAc.setMode(kMitsubishiHeavyCool);
  mitsuAc.setTemp(temp);
  mitsuAc.setSwingVertical(kMitsubishiHeavy152SwingVLow );
  mitsuAc.setSwingHorizontal(kMitsubishiHeavy152SwingHAuto );
  mitsuAc.send();
}


int state = 0;

float temperature = 26.0;
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {

      if(!advertisedDevice.haveServiceUUID()) return;
      if(!advertisedDevice.getServiceUUID().equals(serviceUUID)) return;
      printf("SwitchBot Meter!\n");

      if(!advertisedDevice.haveServiceData()) return;
      std::string s = advertisedDevice.getServiceData();

      if(!advertisedDevice.getServiceDataUUID().equals(serviceDataUUID)) return;

      const char* servicedata = s.c_str();
      int battery = servicedata[2] & 0b01111111;
      bool isTemperatureAboveFreezing = servicedata[4] & 0b10000000;
      temperature = ( servicedata[3] & 0b00001111 ) / 10.0 + ( servicedata[4] & 0b01111111 );
      if(!isTemperatureAboveFreezing){
        temperature = -temperature;
      }
      int humidity = servicedata[5] & 0b01111111;

      printf("battery:     %d\n",   battery);
      printf("temperature: %.1f\n", temperature);
      printf("humidity:    %d\n",   humidity);
      printf("\n");

    }
};


//our callback functions
void terebiChanged(EspalexaDevice *d) {
  if (d == nullptr)
    return; //this is good practice, but not required
  Serial.print("A changed to ");
  Serial.println("ON");
  M5.dis.drawpix(0, 0x00f000);
  irsend.sendNEC(0x15748B7);
  delay(500);
  M5.dis.drawpix(0, 0x000000);
}


void zidouChanged(EspalexaDevice *d) {
  if (d == nullptr) return; 
  M5.dis.drawpix(0, 0x00f000);
  if (d->getValue()){
    Serial.println("Auto AC ON");
    autoControl = true;
    xTaskCreatePinnedToCore(Task1,"Task1", 4096, NULL, 3, NULL, 1);
  }
  else {
    Serial.println("Auto AC OFF");
    autoControl = false;
    ready = false;
  }
  delay(500);
  M5.dis.drawpix(0, 0x000000);
}

void airconChanged(EspalexaDevice *d)
{
  if (d == nullptr)
    return;

  M5.dis.drawpix(0, 0x00f000);

  sendAc(false, 26);
  delay(500);
  M5.dis.drawpix(0, 0x000000);
}
// connect to wifi – returns true if successful or false if not
bool connectWifi()
{
  bool state = true;
  int i = 0;

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");
  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;
}
void Task1(void *pvParameters) {
  while(autoControl) {
    BLEScanResults foundDevices = pBLEScan->start(scanTime, false);
    pBLEScan->clearResults();
    ready = true;
    delay(scanInterval);
  }
  vTaskDelete(NULL);
}

void setup()
{
  Serial.begin(115200);
  M5.begin(true, false, true);

  irsend.begin();
  mitsuAc.begin();
  M5.begin();

  // Initialise wifi connection
  wifiConnected = connectWifi();

  if (!wifiConnected)
  {
    while (1)
    {
      Serial.println("Cannot connect to WiFi. Please check data and reset the ESP.");
      delay(2500);
    }
  }

  // Define your devices here.
//  espalexa.addDevice("TEREBI", terebiChanged, EspalexaDeviceType::onoff);    //non-dimmable device
//  espalexa.addDevice("AIRCON", airconChanged, EspalexaDeviceType::onoff); //Dimmable device, optional 4th parameter is beginning state (here fully on)
  espalexa.addDevice("ZIDOU", zidouChanged, EspalexaDeviceType::onoff); //Dimmable device, optional 4th parameter is beginning state (here fully on)

  espalexa.begin();


  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan(); //create new scan
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks(), true);
  pBLEScan->setActiveScan(true); //active scan uses more power, but get results faster
  pBLEScan->setInterval(10000);
  pBLEScan->setWindow(9999);  // less or equal setInterval value
}

void loop() {
  espalexa.loop();
  M5.update();
  if (M5.Btn.wasPressed())
  {
    Serial.println("Btn.wasPressed() == TRUE");
          //  irsend.sendNEC(0x15748B7);
    M5.dis.drawpix(0, 0x00f000);

    sendAc(true, 25);
    printState();
//    autoControl = true;
//    xTaskCreatePinnedToCore(Task1,"Task1", 4096, NULL, 3, NULL, 1);
    delay(500);
    M5.dis.drawpix(0, 0x000000);
  }
  if (ready) {
    if (27 < temperature) {
      M5.dis.drawpix(0, 0x0000f0);
      sendAc(true, 24);
      printState();
      M5.dis.drawpix(0, 0x000000);
      ready = false;
    }
    else if (temperature < 25) {
      M5.dis.drawpix(0, 0x0000f0);
      sendAc(true, 28);
      printState();
      M5.dis.drawpix(0, 0x000000);
      ready = false;
    }
  }

  delay(1);
}

```

</div>
</details>

Alexaと組み合わせるとこのような感じになります
動画ではAtom lite内蔵赤外線LED(G12)でテレビを、
IR Unit(G26)でエアコンを操作しています
また、「アレクサ、自動オン」としゃべると自動制御が始まります

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/Espalexa?src=hash&amp;ref_src=twsrc%5Etfw">#Espalexa</a> と <a href="https://twitter.com/hashtag/ATOMlite?src=hash&amp;ref_src=twsrc%5Etfw">#ATOMlite</a> と IrRemoteESP8266を組み合わせてテレビとエアコンを操作できたよ～<br><br>GroveのIRユニットで天井のエアコンに赤外線を飛ばしてます<br><br>M5StickCとESP32dev board使ってたけど安いしM5StickCよりGPIO多いしもう10個くらい欲しいｗ<a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://t.co/ytyimrIcxR">pic.twitter.com/ytyimrIcxR</a></p>&mdash; もけ@ムギ㌠ (@coppercele) <a href="https://twitter.com/coppercele/status/1257311278244089858?ref_src=twsrc%5Etfw">May 4, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>