M5Stackシリーズで赤外線リモコンを作り、Swichbot温湿度計の温度を取得してエアコン自動制御します
作ってから気づいたんですけどこれHubの機能そのままですね(´・ω・`)
使うデバイス##
ATOM Lite - スイッチサイエンス
https://www.switch-science.com/catalog/6262/
M5Stackシリーズの一番安い奴です
968円ってESP32 Dev kitより安いんですけど?!
Groveと赤外線LEDが内蔵されてて赤外線リモコンを作るのにうってつけです
M5Stack用赤外線送受信ユニット - スイッチサイエンス
https://www.switch-science.com/catalog/5699/
Atom lite内蔵赤外線LEDは射程距離が短くてエアコンを操作できないのでこれを使います
GroveケーブルでM5Stackシリーズと接続するだけで使えます
Amazonなどで赤外線LEDやレシーバを売ってますが回路を組むのがめんどいのでUnit使った方がいいと思います
ケースに収まってるし安いし(308円)
Amazon.co.jp: SwitchBot スイッチボット デジタル 温湿度計 スマート家電 – 高精度 スイス製センサースマホで温度湿度管理 アラーム付き グラフ記録 Alexa, GoogleHome, IFTTT対応 (ハブ必要): DIY・工具・ガーデン
https://www.amazon.co.jp/dp/B07L4QNZVF/
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
そして温度を取得してその温度に応じて赤外線でコマンドを飛ばせばいいと思ったのですが
なぜか動きません
※ハマリポイントその1
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()の中ではエアコンが反応したのでそのようにしました
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変数がスキャンする周期を設定するのだと思っていたのですが
どうもソースを見る限りタイムアウト時間の設定のようです
※ハマリポイントその2
setup()中に
pBLEScan->setInterval(10000);
pBLEScan->setWindow(9999); // less or equal setInterval value
があったのでこれだ!と思ったんですけどBLEの仕様によって10秒(10000)以上は設定できないようです
なので結局BLEスキャンをマルチスレッドに出してdelay()を入れることにしました
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);
}
エアコン自動制御を構築する##
ハマリポイントを抜けたのでエアコン自動制御するスケッチを作ります
# 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を同時に使用するとスケッチが大きくなりすぎるので
パーテションの設定を変更して対応します
NoOTAヤッター! pic.twitter.com/0yHicae9IO
— もけ@ムギ㌠ (@coppercele) June 26, 2020
EspalexaSwichbot.ino
# 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);
}
Alexaと組み合わせるとこのような感じになります
動画ではAtom lite内蔵赤外線LED(G12)でテレビを、
IR Unit(G26)でエアコンを操作しています
また、「アレクサ、自動オン」としゃべると自動制御が始まります
#Espalexa と #ATOMlite と IrRemoteESP8266を組み合わせてテレビとエアコンを操作できたよ~
— もけ@ムギ㌠ (@coppercele) May 4, 2020
GroveのIRユニットで天井のエアコンに赤外線を飛ばしてます
M5StickCとESP32dev board使ってたけど安いしM5StickCよりGPIO多いしもう10個くらい欲しいw#M5Stack pic.twitter.com/ytyimrIcxR