Smart HomeをDIYする

Apple HomeKit は対応製品が少なくて高価なのでHomebridgeでがんばります

三菱エアコン用スマートリモコンをDIYする

こちらに移転しましたので自動転送します。

エアコンリモコンをコントロールするスマートリモコンをESP32で作りました。以下の記事です。この時はパナソニックのエアコンを対象にしたのですが、今回は三菱のエアコンのためのスマートエアコンを作ります。

diysmarthome.hatenablog.com

ハードウェアとシステムの構成は今までと同じです。下図のように、(右から)HomeKitに接続したHomebridgeサーバから、MQTT経由でコマンドを出し、ESP32で受け取って、赤外線LEDからリモコン信号を送り、エアコンをコントロールします。

 

ユニバーサル基板上に前回作成した回路を使用しました。赤外線LEDを3個搭載して、確認用赤色LEDがついてます。右下に見える黒い部品は温度湿度センサです。

赤外線パターンを確認

対象とする三菱エアコンのリモコンは下のようなものです。現行製品ではなく、古い製品です。上部に穴が開いていてフックに掛けられるのが便利です。裏側に NH112という型番が刻印されてます。

このリモコンからの赤外線パターンを読み込んでみました。使ったのはRaspberry Piに赤外線受信モジュールを取り付けたDIYバイスです。

diysmarthome.hatenablog.com

その結果、赤外線パターンは以下のようになってました。横軸が時間で、縦軸が赤外線強度です。同一データが2回繰り返されていることが確認できました。

 

 

パルス幅のパターンから、家製協フォーマットのようです。1バイトのデータがLSBから先に転送されていると仮定してデコードすると、以下のようでした。まずは、21度冷房のパターンです。これは、リモコンのリセットボタンを押したデフォルト状態です。

23CB260100005805364000000000100000F8 (off)
23CB26010020580536400000000010000018 (on)

次に、27度暖房です。これもリセット後のデフォルト状態です。

23CB26010000480B304000000000100000E8 (off)
23CB26010020480B30400000000010000008 (on)

最後のバイトはチェックサムで、それまでのバイトの合計の256の剰余でした。チェックサムの計算がシンプルに行えるので、LSBから転送されていると考えるのが自然です。

機能への割り当てを探る

この先は、リモコンの設定を色々変えて、ビットへの機能割り当てを探ります。今回は、この世代のリモコンを解析してくれたページがあり、とても参考になりました。型番がNH122だそうで、NH112とは型番が少し異なりますが、信号パターンは同じでした。

qiita.com

このページでフォーマットがまとめられています。

https://camo.qiitausercontent.com/60ad20530cb2a68861767878bf2d203f025284ab/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3234323430322f38323237343836642d393537352d363134372d336335642d3333633865393462343935342e706e67

https://qiita.com/Hiroki_Kawakami/items/37cdb412a4e511a58103

 

例えば、上記のデフォルト状態では、冷暖房に合わせた除湿設定、水平風方向は中央、垂直風方向は自動、風量は自動になっているようです。

HomeKit仕様に合わせた機能選択

HomeKitのHeater Coolerアクセサリを使うので、その仕様に合わせて機能を選択します。

三菱リモコンの運転切り替えを何度か押すと、冷房、除湿、暖房、送風が順番に選択できます。このうちHomeKitで定義されているのは、冷房と暖房だけです。また小さなボタンで「体感」モードのon/off切り替えができます。体感モードはこの世代の三菱エアコンの特徴で、人のいる方向を検知して、そちらを重視して冷やす・温めることで節電しようという仕掛けらしいです。デフォルトで体感モードはonらしいので、onにしておきます。というようなことを考慮して、次の機能を使用することにします。

  • 冷房・暖房・停止をする(ただし体感モードon)
  • 温度調整をする
  • Swing onは水平風向方向スウィングとする
  • Swing offは水平風向方向を中央とする
  • 垂直方向の風向は自動とする(冷暖房に合わせて上下する)
  • 風量はスライダに合わせて、自動、弱、中、強、最強と割り当てる

これに合わせてリモコンデータを変化させると、

  • 運転・停止をする:5バイト目を00または20にする
  • 冷房・暖房を切り替える:6バイト目を58または08にする
  • 温度調整をする:7バイト目に数値を設定
  • Swing onは水平風向方向スウィングとする:8バイト上位4ビットをC
  • Swing offは水平風向方向を中央とする:8バイト上位4ビットを3
  • 除湿は実リモコンと同じ設定:8バイト下位4ビットを冷房は6, 暖房は0
  • 垂直方向の風向は自動とする:9バイト中3ビットを0
  • 風量はスライダに合わせて、自動、弱、中、強、最強と割り当てる:9バイト目下位3ビットを0,1,2,3にする

となります。

赤外線を出すプログラム

これを元に、ESP32にプログラムします。エアコンからHomeKitへの流れのうち、ESP32の部分です。

今回も、IRremoteESP8266というライブラリを使いました。前回は、パナソニックエアコンのクラスを使用しました。今回は、三菱電機のクラスを使います。ライブラリには、三菱電機のものと、三菱重工の2種類がありました。内容を見ると、三菱電機のものが今回のリモコンパターンに近いので、それを使うことにしました。

三菱電機のリモコンも、テレビやエアコン機種ごとに、いくつかの種類が用意されてました。IRMitsubishiACというクラスが、18バイトの信号を出すクラスでした。今回のリモコンと合致するので、これを使いました。それでIRMitsubishiACのインスタンスを作って、温度やon/offをメソッドで設定した後、sendメソッドを呼ぶことにします。sendメソッドの引数は繰り返し回数です。1とすると、本体に加えて1回余分に繰り返すという意味でした。なので1とすると2回繰り返し、実リモコンの動作と一致します。

 

 

HomeKitと連携して設定する部分は、on/off, 冷房暖房切り替え、温度設定、風量設定、スウィングon/offです。このうち、on/off、冷暖房切り替え、温度設定は、IRMitsubishiACクラスのメソッドで問題なく設定できました。一方、風量と風向は実リモコンと振る舞いが違ってしまいました。そこで、この二点については、18バイトの信号を直接書き換えることにしました。

風向の設定

まずは風向の設定です。今回は、HomeKitのスウィング設定で、水平方向の風向を設定することにします。スウィングoffの場合は、水平風向を中央にして、スウィングonの場合は、水平にスウィングさせます。実リモコンのデータを確認すると、リモコンリセット直後(21度冷房)の状態で、水平風向中央と水平風向スウィングの信号パターンは、

23CB2601002058053683000000001010006B center
23CB260100205805C64300000000101000BB swing

でした。太字の8, 9バイト目が変化します。そこでこれを切り替える関数、setSwing()を作りました。引数のswingmodeがtrueかfalseに従い、信号パターンを切り替えます。

void setSwing(bool swingmode){
  uint8_t *raw=mitsubishi.getRaw();
  if(swingmode) {
    raw[8] = (raw[8] & 0x0F) | 0xC0;
    raw[9] = (raw[9] & 0x3F) | 0x40;
  }else{
    raw[8] = (raw[8] & 0x0F) | 0x30;
    raw[9] = (raw[9] & 0x3F) | 0x80;
  }
}

風量の設定

HomeKitでは風量を0から100で設定します。これを適当に切り分けて、自動・弱・中・強・最強に割り当てました。 これもリモコンリセット直後(21度冷房)の状態で、風量切り替えた信号を取得しました信号を取得しました。 その結果、自動・弱・中・強・最強のパターンは、

23CB26010020580536800000000010000058 auto
23CB26010020580536410000000010000019 low
23CB2601002058053642000000001000001A mid
23CB2601002058053643000000001000001B high
23CB2601002058053643000000001010002B powerfull

でした。太字の部分が変化してますが、これもIRMitsubishiACの実装と違う動きをしていました。そこで変化する部分、9バイト目と15バイト目を書き換えることにします。引数は、HomeKitで指定されたファン強度です。

void setSpeed(int speed){
  uint8_t *raw=mitsubishi.getRaw();
  raw[15] = raw[15] & 0xEF; //clear the powerfull bit(?)
  if     (speed < 20) raw[9] = (raw[9] & 0x3C) | 0x80; //auto
  else if(speed < 40) raw[9] = (raw[9] & 0x3C) | 0x41; //low
  else if(speed < 60) raw[9] = (raw[9] & 0x3C) | 0x42; //mid
  else if(speed < 80) raw[9] = (raw[9] & 0x3C) | 0x43; //high
  else { //powerfull
    raw[9] = (raw[9] & 0xC3) | 0x43;
    raw[15] = raw[15] | 0x10; //set the powerfull bit.
  }
}

MQTTと温度センサを追加

これ以外は、前回のパナソニックと同様です。MQTTメッセージを受け取る部分と、温度センサDTH20のI2C部分

を追加して、以下のように完成させました。

/* IRremoteESP8266 over MQTT */

#include <Arduino.h>
#include <ArduinoOTA.h>
#include <cstring> //for std::memcpy method

//IR Remote
#include <IRremoteESP8266.h>
#include <ir_Mitsubishi.h>
const uint16_t kIrLed = 4;  // ESP8266 GPIO pin to use.
IRMitsubishiAC mitsubishi=IRMitsubishiAC(kIrLed);
//DHT sensor
#include "DHT20.h"
#define GPIO_SDA 21 //I2C for DHT20
#define GPIO_SCL 22 //I2C for DHT20
DHT20 dht; //instance of DHT20
//MQTT
#include <EspMQTTClient.h>
EspMQTTClient *client; //instance of MQTT client

//WiFi & MQTT
const char SSID[] = "XXXXXXXX"; //WiFi SSID
const char PASS[] = "xxxxxxxx"; //WiFi password
char CLIENTID[] = "IRremote_2927595"; //something random
const char  MQTTADD[] = "192.168.xxx.xxx"; //Broker IP address
const short MQTTPORT = 1883; //Broker port
const char  MQTTUSER[] = "xxxxxx";//Can be omitted if not needed
const char  MQTTPASS[] = "XXXXXX";//Can be omitted if not needed
const char  SUBTOPIC[] = "mqttthing/irOffice/set/#"; //subscribe
const char  PUBTOPIC[] = "mqttthing/irOffice/get"; //publish temp
const char  DEBUG[] = "mqttthing/irOffice/debug"; //for debug

//base values for the IR Remote state (Mitsubishi AC)
uint8_t coolState [kMitsubishiACStateLength] = { //28deg. cool, auto, Taikan
0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x58, 0x0C, 0x36, 0x40, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x18
};
uint8_t heatState [kMitsubishiACStateLength] = { //21deg. heat, auto, Taikan
0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x48, 0x05, 0x30, 0x40, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x18
};

void setSwing(bool swingmode){
  uint8_t *raw=mitsubishi.getRaw();
  if(swingmode) {
    raw[8] = (raw[8] & 0x0F) | 0xC0;
    raw[9] = (raw[9] & 0x3F) | 0x40;
  }else{
    raw[8] = (raw[8] & 0x0F) | 0x30;
    raw[9] = (raw[9] & 0x3F) | 0x80;
  }
}

void setSpeed(int speed){
  uint8_t *raw=mitsubishi.getRaw();
  raw[15] = raw[15] & 0xEF; //clear the powerfull bit(?)
  if     (speed < 20) raw[9] = (raw[9] & 0x3C) | 0x80; //auto
  else if(speed < 40) raw[9] = (raw[9] & 0x3C) | 0x41; //low
  else if(speed < 60) raw[9] = (raw[9] & 0x3C) | 0x42; //mid
  else if(speed < 80) raw[9] = (raw[9] & 0x3C) | 0x43; //high
  else { //powerfull
    raw[9] = (raw[9] & 0xC3) | 0x43;
    raw[15] = raw[15] | 0x10; //set the powerfull bit.
  }
}

void setup() {
  dht.begin(GPIO_SDA, GPIO_SCL); //DHT20
  mitsubishi.setRaw(coolState); //initialize the raw value
  mitsubishi.begin();
  client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT); 
  delay(1000);
}

void onMessageReceived(const String& topic, const String& message) { 
  String command = topic.substring(topic.lastIndexOf("/") + 1);

  if (command.equals("Active")) {
    if(message.equalsIgnoreCase("true")) mitsubishi.on();
    if(message.equalsIgnoreCase("false")) mitsubishi.off();
    mitsubishi.send(1);
  }else if(command.equals("TargetHeaterCoolerState")){
    if(message.equalsIgnoreCase("COOL")) {
      switchMode(kMitsubishiAcCool);
    }
    if(message.equalsIgnoreCase("HEAT")) {
      switchMode(kMitsubishiAcHeat);
    }
  }else if(command.equals("CoolingThresholdTemperature")){
    mitsubishi.setTemp(message.toFloat());
    mitsubishi.send(1);
  }else if(command.equals("HeatingThresholdTemperature")){
    mitsubishi.setTemp(message.toFloat());
    mitsubishi.send(1);
  }else if(command.equals("SwingMode")){
    if(message.equalsIgnoreCase("DISABLED")) setSwing(false);
    if(message.equalsIgnoreCase("ENABLED")) setSwing(true);
    mitsubishi.send(1);
  }else if(command.equals("RotationSpeed")){
    setSpeed(message.toInt());
  }
}

void onConnectionEstablished() {
  ArduinoOTA.setHostname("irXXXXXX");
  ArduinoOTA.setPasswordHash("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
  ArduinoOTA.begin();
  Serial.println("MQTT connection established.");
  client->subscribe(SUBTOPIC, onMessageReceived); //set callback function
  client->publish(DEBUG,"irXXXXXX started.");
}

//Backup and switch the operating mode.
void switchMode(const uint8_t targetmode){
  uint8_t currentmode;
  
  currentmode = mitsubishi.getMode();
  if(targetmode == currentmode) return; //no switch, do nothing

  const uint8_t *raw=mitsubishi.getRaw();
  
  if((currentmode == kMitsubishiAcHeat) && (targetmode == kMitsubishiAcCool)) {
    std::memcpy(heatState, raw, kMitsubishiACStateLength);
    mitsubishi.setRaw(coolState);
  }
  if((currentmode == kMitsubishiAcCool) && (targetmode == kMitsubishiAcHeat)) {
    std::memcpy(coolState, raw, kMitsubishiACStateLength);
    mitsubishi.setRaw(heatState);
  }
}

//IR Remo and MQTT: read DHT20 and publish results
void publishDHT() { 
  char buff[64];
  float humi, temp;
  if(DHT20_OK != dht.read()){
    client->publish(DEBUG,"DHT20 Read Error.");
  }else{
    humi=dht.getHumidity();
    temp=dht.getTemperature();
    sprintf(buff, "{\"temperature\":%.1f,\"humidity\":%.0f}", temp, humi);
    client->publish(PUBTOPIC,buff);
  }
}

void loop() {
  ArduinoOTA.handle();
  client->loop(); 
  if(millis() - dht.lastRead() >= 60000) publishDHT();
}

HomeKitから使用する

ここから先はRaspberry Piでの設定部分です。

 

 

Mqttthingプラグインを使い、Heater Coolerアクセサリを実装します。その設定で、

{
            "type": "heaterCooler",
            "name": "Aircon",
            "url": "mqtt://localhost:1883",
            "topics": {
                "setActive": "mqttthing/irOffice/set/Active",
                "setCoolingThresholdTemperature": "mqttthing/irOffice/set/CoolingThresholdTemperature",
                "getCurrentTemperature": "mqttthing/irOffice/get$.temperature",
                "setHeatingThresholdTemperature": "mqttthing/irOffice/set/HeatingThresholdTemperature",
                "setRotationSpeed": "mqttthing/irOffice/set/RotationSpeed",
                "setSwingMode": "mqttthing/irOffice/set/SwingMode",
                "setTargetHeaterCoolerState": "mqttthing/irOffice/set/TargetHeaterCoolerState"
            },
            "restrictHeaterCoolerState": [
                1,
                2
            ],
            "accessory": "mqttthing"
        },

のように設定しました。この結果、iPhoneMacのホームには、こんな形で現れます。

クリックすると、On/off、温度調整、動作モード切り替え(冷房・暖房・自動)を行うウィンドウが開きます。

また、このウィンドウの歯車アイコンをクリックすると、「ファンの速さ」が0から100までのスライダで調節でき、さらに「首振り」をスイッチでon/offできます。ファンの速さは、数値が低い方から自動・弱・中・強・最強にマッピングされてます。また首振りは、水平方向のスウィングに割り当ててあります。

まとめ

前回はパナソニックのエアコンを対象としましたが、今回は三菱のエアコンを対象にして、スマートリモコンをDIYしました。IRremoteESP8266ライブラリは多数のメーカの基本機能に正しく対応しています。しかし風量・風向機能で実際のリモコンと動作が違っていましたので調整しました。

製品世代の違いや販売地域の違いで、ライブラリの動作と実際のリモコンの差異が生じているようです。これは製品版のスマートリモコンでも同じことだと思います。そこをきっちりと合わせられるのがDIYの良いところだと思いました。