ダイキンエアコン用スマートリモコンをDIYする

DIYする

ダイキンのエアコンために、赤外線リモコンとして動作するスマートリモコンを作ります。以前にも、パナソニックと三菱のエアコンを対象にDIYしました。今回はダイキンです。

追記:後半のESP32のソフトウェアを、近いうちに改変したいと考えてます。その時に更新します(2023/02/04)

全体構成とハードウェア

システム構成とハードウェアは今までと同じです。下図のように、(右から)HomeKitに接続したHomebridgeサーバから、MQTT経由でコマンドを出し、ESP32で受け取って、赤外線LEDからリモコン信号を送り、エアコンをコントロールします。Heater coolerアクセサリには、温度測定機能も必要なので、I2C接続の温度・湿度センサDHT20も取り付けました。

スマートリモコン作成のために10枚発注したプリント基板がまだまだ残っているので、配線にはこれを使いました。

下が組み立てた様子です。前回の写真とそっくりですがこれは2号機です。赤外線LEDを3個搭載して、確認用LEDがついてます。右下に見える黒い部品が温度湿度センサDHT20です。

ランド間の狭いFETハンダ付け部分も前回よりかなり上手くなりました。

赤外線パターンを確認

対象とするダイキンエアコンのリモコンは以下です。現行製品ではなく、古い製品です。ARC468A3という型番が刻印されてます。

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

その結果、赤外線パターンは以下のようになってました。横軸が時間で、縦軸が赤外線強度です。

他社のリモコンと違って特徴的な点は、最初に6回のパルスが送出される点です。下に冒頭部分の拡大図を示します。同期用もしくは受信側スリープを復帰させる信号と思われます。Arduinoのライブラリではこの部分がLeaderと名付けられてました。

その後、休止期間を挟んで、データが2系列送出されていることが確認できました。最初のグループを1フレーム目、次を 2フレーム目と呼ぶことにします。

フレームは、そのパルス幅パターンから、それぞれのフレームは家製協フォーマットのようです。1バイトのデータがLSBから先に転送されていると仮定してデコードしたところ、チェックサムの計算も合いました。その結果、27度、暖房on、風量最強、風向スウィングonの場合、データの内容は以下でした。1行目が1フレーム目、 2行目が2フレーム目です。それぞれ20, 19バイト、合計で39バイト、132ビットです。

11DA270001000000000000000000000000000013
11DA2700004936007F000006600000C19000C7

過去のパナソニック、三菱の解析結果と同様、フレーム最後(行末)のバイト(13とC7)はチェックサムで、それまでのバイトの合計の256の剰余でした。

リモコン機能を探る

この先は、リモコンの設定を色々変えて、赤外線信号ビットへの機能割り当てを探ります。リモコンの型番で検索したところ、ダイキンのエアコンリモコンを解析してくださっているサイトが見つかり、とても参考になりました。

ダイキンのエアコンのリモコンを解析する
身の回りの家電には赤外線リモコンで操作するものがたくさんあります。エアコンもその一つです。それらのリモコンと同じ赤外線信号を送れるデバイスを作ることができれば、例えばネットワーク経由で本来リモコンが届かないようなところから家電を操作すること...

ただ、リモコンが違うこともあり、多少内容が異なるようです。

1フレーム目は固定

上記のサイトによると、更新される項目により1フレーム目が変化するらしいです。しかし、今回調べた範囲では、1フレーム目の内容は変化せず、固定でした。そのほかの割り当ては、上記のサイトに書いてある内容と一致しました。

On/Offと動作モード

これ以降、フレーム最初のバイトを0番目として数えることにします。

2フレーム目の5バイト目でon/offと動作モードを指定します。HomeKitで使用する動作モードは冷房と暖房だけなので、それだけを調べます。5バイト目LSBがon/offで、1がonで0がoffです。5バイト目MSB側3ビットが運転モードで、冷房は3、暖房は4です。

例えば、暖房offは以下です。当該バイトは0x48で、偶数なのでoff、上位4ビットは4なので暖房です。0x08の1が立っているビットは常時1です。なので、下位4ビットは、offが8、onが9になります。

11DA270001000000000000000000000000000013
11DA27000048360070000006600000C19000B7

一方で、冷房onは以下です。当該バイトは0x39で、奇数なのでon、上位4ビットが3なので冷房です。

11DA270001000000000000000000000000000013
11DA27000039340070000006600000C19000A6

温度設定

温度設定は2フレーム目の6バイト目に、摂氏の温度を2倍した値が入ります。なので仕様上は0.5度単位で指定できますが、実際のリモコンの設定温度は1度単位です。上記サイトの解析によると、6バイト目MSBが1の場合は相対温度設定らしいですが、このリモコンでは0しか観測されませんでした。

26度冷房onの場合が以下です。0x34なので1/2すると10進で26です。

11DA270001000000000000000000000000000013
11DA27000039340070000006600000C19000A6

25度冷房onの場合が以下です。0x32なので1/2すると10進で25です。

11DA270001000000000000000000000000000013
11DA27000039320070000006600000C19000A4

風量と風向

前述のサイトによると、2フレーム目の8バイト目のMSB4ビットが風量、8バイト目LSB4ビットが上下風向、9バイト目LSB4ビットが左右風向だそうです。風向は、0x0が固定で、0xFがスウィングモードです。でもこのリモコンには、上下風向スウィング設定機能しかないので、9バイト目は0x00のままでした。風量は、「しずか」「1」「2」「3」「4」「5」「自動」の順に、{0xB0, 0x30, 0x40, 0x50, 0x60, 0x70, 0xA0} です。

例えば、風量2で上下スウィングonの場合の8バイト目は、風量の0x40と、風向の0x0FのORになり、0x4Fになります。

11DA270001000000000000000000000000000013
11DA2700004936004F000006600000C1900097

風量自動でスウィングoffの場合は、風量が0xA0で、風向が0x00なので、0xA0になります。

11DA270001000000000000000000000000000013
11DA270000493600A0000006600000C19000F7

パナソニックや三菱の場合に比べて、全体にわかりやすいデータ構造のように感じました。

HomeKit仕様に対応する赤外信号

HomeKitのHeater Coolerアクセサリを使うので、仕様で定義されている「運転・停止・冷房・暖房・温度・Swing・風量」が設定できれば十分です。なので、ここまで確認した部分だけを変化させるコードを用意できれば良いです。いずれも 2フレーム目への対応です。

  • 運転:5バイト目 = (5バイト目 & 0xFE ) | 0x01
  • 停止:5バイト目 = (5バイト目 & 0xFE )
  • 冷房:5バイト目 = (5バイト目 & 0x0F) | 0x30
  • 暖房:5バイト目 = (5バイト目 & 0x0F) | 0x40
  • 温度設定:6バイト目 = 設定温度の2倍
  • Swing on:8バイト目 = (8バイト目 & 0xF0) | 0x0F
  • Swing off:8バイト目 = (8バイト目 & 0xF0) | 0x00
  • 風量はスライダに合わせて、{自動、しずか、1, 2, 3, 4, 5} と割り当て、
    8バイト目 = (8バイト目 & 0x0F) | {0xA0, 0xB0, 0x30, 0x40, 0x50, 0x60, 0x70}

という赤外線データが送出できることを目標にします。

ESP32で赤外線プログラミング

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

今回も、IRremoteESP8266というライブラリを使いました。

GitHub - crankyoldgit/IRremoteESP8266: Infrared remote library for ESP8266/ESP32: send and receive infrared signals with multiple protocols. Based on: https://github.com/shirriff/Arduino-IRremote/
Infrared remote library for ESP8266/ESP32: send and receive infrared signals with multiple protocols. Based on: - GitHub...

前回は、パナソニックと三菱電機のクラスを使用しました。今回はダイキンのクラスを使います。

使用するクラスを選ぶ

IRremoteESP8266のsrcディレクトリを調べて、使い方を探ります。この中にあるir_Daikin.hを見ると、数種類のクラスが用意されています。使用するダイキンリモコンの機種に合ったクラスを選ぶ必要があります。ソースコードのコメントには、サポートされているリモコン型番も書いてありました。ただ、今回のリモコンの型番ARC468A3に近いものを探したところ、ARC46…までは合致したものがありましたが、それ以降の数値が一致するリモコンはありませんでした。サポートされていないようですので、仕様が近いクラスを探すことにしました。

用意されているクラス定義には、それぞれ

/// Class for handling detailed Daikin 64-bit A/C messages.

というようなコメントがありました。A/C messagesというのは、赤外線リモコンで送信する情報と思われます。今回は、1フレーム目が20バイト、2フレーム目が19バイトで、合計39バイト、312ビットです。クラス定義を探すと、312-bitのコメントが見つかりました。

/// Class for handling detailed Daikin 312-bit A/C messages.
class IRDaikin2 {

そこで、このIRDaikin2というクラスを使うことにしました。

IRDaikin2クラスの使い方

プログラミングは、以前のパナソニック、三菱電機のリモコンの場合と同様です。今回は、IRDaikin2クラスを同じように使用します。

まずは、プログラムの冒頭で、以下のようにヘッダファイルをインクルードして、赤外線LEDを接続したGPIOピン番号を指定して、IRDaikin2クラスのインスタンスを作ります。インスタンスの名前はdaikinにしました。

#include <IRremoteESP8266.h>
#include <ir_Daikin.h>
const uint16_t kIrLed = 4; // ESP8266 GPIO pin to use. Recommended: 4 (D2).
IRDaikin2 daikin=IRDaikin2(kIrLed);

setup()関数では、これを初期化します。今回も、実リモコンで得たパターンを初期値として用意して、setRaw()メソッドを使って設定しておきました。下のコードのsetup()では、リモコンの他に、温度センサDHT20とMQTTの初期化もしています。

//base values for the IR Remote state (Daikin AC)
uint8_t coolState [kDaikin312StateLength] = { //28deg. off, cool, max, noswing
0x11, 0xDA, 0x27, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13,
0x11, 0xDA, 0x27, 0x00, 0x00, 0x38, 0x38, 0x00, 0x70, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC1, 0x90, 0x00, 0xA9
};

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

あとは、このインスタンス(名前はdaikin)に、on/off, 運転状態、温度、風量、風向を設定して、send()関数を使って赤外線送出すれば良いです。例えば、温度を28度に設定して、赤外線を送るには、

daikin.setTemp(28); //引数は温度
daikin.send(0); //引数は繰り返し回数、実リモコンに合わせて一回だけ送出

のようにします。このほかに、daikin.on(), daikin.off(), daikin.setSwingVertical()などの関数で、on/off/スィング設定などができます。

実リモコンのフレームに一致させる

ただ、クラスの関数(インスタンスメソッド)をそのまま使うと、操作によっては、1フレーム目までもが変化してしまいました。実リモコンの操作では1フレーム目は変化しません。IRDaikin2の関数を使うと、停止の操作とスウィングの操作で、1フレーム目が変化してしまいます。前述のweb上の情報でも1フレーム目は変化する場合があるとのことなので、手元のリモコンが特殊なのかもしれません。

ここは実リモコンに合わせることにします。そこで、1フレーム目が変化してしまう操作に対しては、IRDaikin2が提供するメソッドはそのまま使わないで、直接データを書き換えることにしました。パナソニックや三菱のエアコンの時と同じです。getRaw()関数で状態データへのポインタを取得して、前節の方針で、該当する箇所を書き換えます。

getRaw()関数で得られるのは、第1フレーム(20バイト)と 第2フレーム(19バイト)を連結したデータ(合計39バイト)へのポインタです。前節で数えた第2フレームのバイト数には、20を足せばgetRaw()で得られる配列要素番号になります。

例えば、on/offは第2フレーム5バイト目(第1フレームから数えると25バイト目)のLSBを1または0にすれば良いので、

uint8_t *raw=daikin.getRaw(); //get pointer to the IR state data
raw[25] = (raw[25] & 0xFE) | 0x01; //on

または、

raw[25] = (raw[25] & 0xFE); //off

とします。

実リモコンのリーダーに一致させる

こうして作った赤外線パターンを、実リモコンと比較しました。

上が実リモコンで、下がESP32が生成したパターンです。25度、冷房、on, 風量最高、スィングoffのパターンです。フレーム1も2も同一であることがわかります。よかったです。でも、冒頭の部分が少し違ってます。実リモコンでは6回点滅しているリーダー部分が、ESP32のパターンでは連続点灯になってます。

実はエアコンはこれでも問題なく反応しました。前述のwebにも、リーダー部分が無くても動作すると書かれてました。リーダー部分は、スリープ状態になったエアコンを目覚めさせるためのパルスではないかと思われます。なので、確実にリモコンを機能させるためには、この部分も実現したいです。スマートリモコンとしては、1回目の信号で成功して欲しいところです。

Arduinoにインストールしたライブラリは、~/Documents/Arduino/librariesに入ります。その中の

src/ir_Daikin.cpp

を探したところ、IRDaikin2::send()関数は、662行目あたりで定義されている、IRsend::sendDaikin2()を呼び出しているだけでした。

この関数には、リーダー、フレーム1、フレーム2を送出する部分が書かれてました。それぞれのパートをコメントアウトしてみると、その部分が無い赤外線が送出されるので、このことを確認できました。また、リーダー、フレーム1、フレーム2を送出する部分は、それぞれIRsend::sendGeneric()というメソッドを呼び出しているだけです。sendGeneric()は、送信したいデータをバイト配列で指定し、スタート、ストップ、0, 1 などを構成する赤外線パルス幅を指定し、赤外線を送出する基本的な関数です。

このIRsend::sendDaikin2()をお手本に、自前のsend()関数を作ってしまいました。名前は、send_daikin()です。関数の定義のほかに、IRsendクラスのインスタンスメソッドを呼び出すことになるので、インスタンスも作っておきます。また、6個のパルスを表現したリーダーのパターンも定義しておきます。

IRsend irsend(kIrLed); //for sending raw IR data
const uint16_t kDaikinLeader[12]= { 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 25300 };

void send_daikin(){ //original is IRsend::sendDaikin2()
  uint8_t *data=daikin.getRaw();
  // Leader
  irsend.sendRaw(kDaikinLeader, 12, 38);//send 12 bytes with 38kHz
  // Section #1
  irsend.sendGeneric(kDaikin2HdrMark, kDaikin2HdrSpace, kDaikin2BitMark,
    kDaikin2OneSpace, kDaikin2BitMark, kDaikin2ZeroSpace,
    kDaikin2BitMark, kDaikin2Gap, data, kDaikin2Section1Length,
    kDaikin2Freq, false, 0, 50);
  // Section #2
  irsend.sendGeneric(kDaikin2HdrMark, kDaikin2HdrSpace, kDaikin2BitMark,
    kDaikin2OneSpace, kDaikin2BitMark, kDaikin2ZeroSpace,
    kDaikin2BitMark, kDaikin2Gap, data + kDaikin2Section1Length,
    kDaikin2Section2Length, kDaikin2Freq, false, 0, 50);
}

あとはプログラムの中で、リモコンデータを送出する場面で、daikin.send(0)の代わりに、この関数、send_daikin()呼びだします。例えば前述の、「温度を28度に設定して赤外線を送る」場合には、以下のようにします。

daikin.setTemp(28); //引数は温度
//daikin.send(0); //daikin.send()を呼び出す代わりに
daikin_send();  //自前で作ったdaikin_send()を呼び出す

この結果得られたESP32の赤外線信号を、再び実リモコンと比較します。上が実リモコンで、下がESP32の信号です。(データの内容は、25度、冷房、on, 風量最高、スィングoff)

今度は、リーダーを含めて、全てがピッタリと一致しました!ESP32でMQTTプログラミング

この先は、パナソニック、三菱の時と同様です。MQTTのメッセージに合わせて、赤外線を送出するようにプログラミングします。冷房・暖房の切り替えの際に、暖房・冷房のデータを保存して、次の切り替え時に復活させるようにもプログラミングしました。これも前回と同様です。

変数を設定しておいて、setup()で起動して、loop()では定期的にハンドラーを呼び出します。接続が確立した段階で呼ばれるonMessageReceived()関数でcallback関数を設定します。onMessageReceived()関数では、トピックとメッセージの内容に合わせて、赤外線データを作成して発信します。

//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_07649874"; //something random
const char  MQTTADD[] = "xxx.xxx.xxx.xxx"; //Broker IP address
const short MQTTPORT = 1883; //Broker port
const char  MQTTUSER[] = "";//Can be omitted if not needed
const char  MQTTPASS[] = "";//Can be omitted if not needed
const char  SUBTOPIC[] = "mqttthing/irLiving/set/#"; //mqtt topic to subscribe
const char  DEBUG[] = "mqttthing/irLiving/debug"; //topic for debug
(略)

void setup() {
(略)
  client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT); 
(略)
}

void onMessageReceived(const String& topic, const String& message) { 
(略)
  if (command.equals("Active")) {
(略)
  }else if(command.equals("TargetHeaterCoolerState")){
(略)
  }else if(command.equals("CoolingThresholdTemperature")){
(略)
  }else if(command.equals("HeatingThresholdTemperature")){
(略)
  }else if(command.equals("SwingMode")){
(略)
}

void onConnectionEstablished() {
  client->subscribe(SUBTOPIC, onMessageReceived); //set callback function
(略)
}

void loop() {
  client->loop(); 
(略)
}

ESP32で温度計プログラミング

さらに、温度センサーDHT20に関するコードも作ります。

これも前回と同じです。詳細はこちらをご覧ください。

測定結果はMQTTメッセージで送信します。温度は10秒ごとにチェックします。それで変化していればすぐにMQTTメッセージを流します。変化がなくても5分に1回はMQTTにメッセージを流すようにしました。

#define GPIO_SDA 21 //I2C for DHT20
#define GPIO_SCL 22 //I2C for DHT20
(略)

 void setup() { 
dht.begin(GPIO_SDA, GPIO_SCL); //DHT20 daikin.setRaw(coolState); //initialize the raw value daikin.begin(); Serial.begin(115200); while (!Serial); // wait for serial port to connect. client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT); delay(1000); }
(略)
}

//IR Remo and MQTT: read DHT20 and publish results
//check every 10 sec.
//if temp or humi change publish soon, otherwise publish every 5 min.
void publishDHT() { 
  char buff[64];
  static int count10=0; //counts up in every 10 sec.
  float humi, temp;
  static int oldhumi=0, oldtemp=0; //previous value
  int newhumi, newtemp; //new value
  if(millis() - dht.lastRead() < 10000) return;//do nothing < 10 sec. count10++; //count up 10 s counter if(DHT20_OK != dht.read()){ client->publish(DEBUG,"DHT20 Read Error.");
    return; //sensor error, do nothing.
  }
  //read the current temp and humi
  humi=dht.getHumidity();
  newhumi=round(humi);//int version
  temp=dht.getTemperature();
  newtemp=round(temp * 10);//int version (x10)
  //if measurement changes or 300 seconds passed
  if((oldtemp != newtemp) || (oldhumi != newhumi) || (count10 >= 30)){
    oldtemp=newtemp;
    oldhumi=newhumi;    
    sprintf(buff, "{\"temperature\":%.1f,\"humidity\":%.0f}", temp, humi);
    client->publish(PUBTOPIC,buff);
    count10=0; //reset counter
  }
}

void loop() {
(略)
  client->loop(); 
  publishDHT(); //publish temp and humi if needed
}

OTAにも対応

ネット経由でプログラムをアップデートするOTA (Over The Air) にも対応しました。詳細はこちらをご覧ください。

冒頭でArduinoOTA.hをインクルードして、MQTT接続が確立できたタイミング(=WiFiが接続できたタイミング)でOTAの初期化をして、あとはloop()で定期的にOTAを呼び出します。

#include <ArduinoOTA.h>
(略)

void onConnectionEstablished() {
  ArduinoOTA.setHostname("irLiving");
  ArduinoOTA.setPasswordHash("xxxxxxxxxxxxxxxxxxxxxxx");
  ArduinoOTA.begin();
(略)
}

void loop() {
  ArduinoOTA.handle();
(略)
}

プログラムリスト

以下が全部のプログラムです。長くてすみません。大筋はすでに説明したので、掲載だけにしておきます。

/* IRremoteESP8266 over MQTT */

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

//IR Remote
#include <IRremoteESP8266.h>
#include <ir_Daikin.h>
const uint16_t kIrLed = 4;  // ESP8266 GPIO pin to use. Recommended: 4 (D2).
IRDaikin2 daikin=IRDaikin2(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_73140574"; //something random
const char  MQTTADD[] = "192.168.xxx.xxx"; //Broker IP address
const short MQTTPORT = 1883; //Broker port
const char  MQTTUSER[] = "";//Can be omitted if not needed
const char  MQTTPASS[] = "";//Can be omitted if not needed
const char  SUBTOPIC[] = "mqttthing/irLiving/set/#"; //mqtt topic to subscribe
const char  PUBTOPIC[] = "mqttthing/irLiving/get"; //to publish temperature
const char  DEBUG[] = "mqttthing/irLiving/debug"; //topic for debug

//base values for the IR Remote state (Daikin AC)
uint8_t coolState [kDaikin312StateLength] = { //28deg. off, cool, max, noswing
0x11, 0xDA, 0x27, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13,
0x11, 0xDA, 0x27, 0x00, 0x00, 0x38, 0x38, 0x00, 0x70, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC1, 0x90, 0x00, 0xA9
};
uint8_t heatState [kDaikin312StateLength] = { //25deg, off, heat, max, noswing
0x11, 0xDA, 0x27, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13,
0x11, 0xDA, 0x27, 0x00, 0x00, 0x48, 0x32, 0x00, 0x70, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC1, 0x90, 0x00, 0xB3
};

//Daikin IR remote Leader data
IRsend irsend(kIrLed);  //for sending raw IR data
const uint16_t kDaikinLeader[12]= { 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 420, 25300 };

void send_daikin(){
  uint8_t *data=daikin.getRaw();
  // Leader 
  irsend.sendRaw(kDaikinLeader, 12, 38);//send 12 bytes with 38kHz 
  // Section #1 
  irsend.sendGeneric(kDaikin2HdrMark, kDaikin2HdrSpace, kDaikin2BitMark, kDaikin2OneSpace, kDaikin2BitMark, kDaikin2ZeroSpace, kDaikin2BitMark, kDaikin2Gap, data, kDaikin2Section1Length, kDaikin2Freq, false, 0, 50); 
  // Section #2 
  irsend.sendGeneric(kDaikin2HdrMark, kDaikin2HdrSpace, kDaikin2BitMark, kDaikin2OneSpace, kDaikin2BitMark, kDaikin2ZeroSpace, kDaikin2BitMark, kDaikin2Gap, data + kDaikin2Section1Length, kDaikin2Section2Length, kDaikin2Freq, false, 0, 50); 
}

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

//just for debug
void debugRaw(){
  const uint8_t *raw=daikin.getRaw();
  char buf[kDaikin312StateLength * 2 + 1];
  int i;
  for(i=0;i<kDaikin312StateLength;i++){
    sprintf(&buf[i*2],"%02X",raw[i]);
  }
  buf[kDaikin312StateLength * 2]=0;
  client->publish(DEBUG,buf);
}

//when an MQTT message is received
void onMessageReceived(const String& topic, const String& message) { 
  String command = topic.substring(topic.lastIndexOf("/") + 1);
  
  uint8_t *raw=daikin.getRaw(); //get pointer to the IR state data

  if (command.equals("Active")) {
    if(message.equalsIgnoreCase("true")) raw[25] = (raw[25] & 0xFE) | 0x01; //on
    if(message.equalsIgnoreCase("false")) raw[25] = (raw[25] & 0xFE); //off
    daikin_send();
    debugRaw();
    client->publish(DEBUG,daikin.toString());
  }else if(command.equals("TargetHeaterCoolerState")){
    if(message.equalsIgnoreCase("COOL")) {
      switchMode(kDaikinCool);
    }
    if(message.equalsIgnoreCase("HEAT")) {
      switchMode(kDaikinHeat);
    }
  }else if(command.equals("CoolingThresholdTemperature")){
    daikin.setTemp(message.toInt());
    daikin_send();
    debugRaw();
    client->publish(DEBUG,daikin.toString());
  }else if(command.equals("HeatingThresholdTemperature")){
    daikin.setTemp(message.toInt());
    daikin_send();
    debugRaw();
    client->publish(DEBUG,daikin.toString());
  }else if(command.equals("SwingMode")){
    if(message.equalsIgnoreCase("DISABLED")) raw[28] = (raw[28] & 0xF0) | 0x00; //no swing    
    if(message.equalsIgnoreCase("ENABLED"))  raw[28] = (raw[28] & 0xF0) | 0x0F; //swing
    daikin_send();
    debugRaw();
    client->publish(DEBUG,daikin.toString());
  }else if(command.equals("RotationSpeed")){
    int speed = message.toInt();
    if     (speed < 20) raw[28] = (raw[28] & 0x0F) | 0xA0; //auto
    else if(speed < 40) raw[28] = (raw[28] & 0x0F) | 0xB0; //sizuka
    else if(speed < 50) raw[28] = (raw[28] & 0x0F) | 0x30; //level-1
    else if(speed < 60) raw[28] = (raw[28] & 0x0F) | 0x40; //level-2
    else if(speed < 70) raw[28] = (raw[28] & 0x0F) | 0x50; //level-3
    else if(speed < 80) raw[28] = (raw[28] & 0x0F) | 0x60; //level-4
    else                   raw[28] = (raw[28] & 0x0F) | 0x70; //level-5 max
   }
}

//when MQTT connection is established
void onConnectionEstablished() {
  ArduinoOTA.setHostname("irLiving");
  ArduinoOTA.setPasswordHash("xxxxxxxxxxxxxxxx");
  ArduinoOTA.begin();
  client->subscribe(SUBTOPIC, onMessageReceived); //set callback function
  client->publish(DEBUG,"irOffice started.");
}

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

  const uint8_t *raw=daikin.getRaw();
  
  if((currentmode == kDaikinHeat) && (targetmode == kDaikinCool)) {
    std::memcpy(heatState, raw, kDaikin312StateLength);
    daikin.setRaw(coolState);
  }
  if((currentmode == kDaikinCool) && (targetmode == kDaikinHeat)) {
    std::memcpy(coolState, raw, kDaikin312StateLength);
    daikin.setRaw(heatState);
  }
}

//read DHT20 and publish results
//check every 10 sec.
//if temp or humi change publish soon, otherwise publish every 5 min.
void publishDHT() { 
  char buff[64];
  static int count10=0; //counts up in every 10 sec.
  float humi, temp;
  static int oldhumi=0, oldtemp=0; //previous value
  int newhumi, newtemp; //new value
  if(millis() - dht.lastRead() < 10000) return;//do nothing < 10 sec.
  count10++; //count up 10 s counter
  if(DHT20_OK != dht.read()){
    client->publish(DEBUG,"DHT20 Read Error.");
    return; //sensor error, do nothing.
  }
  //read the current temp and humi
  humi=dht.getHumidity();
  newhumi=round(humi);//int version
  temp=dht.getTemperature();
  newtemp=round(temp * 10);//int version (x10)
  //if measurement changes or 300 seconds passed
  if((oldtemp != newtemp) || (oldhumi != newhumi) || (count10 >= 30)){
    oldtemp=newtemp;
    oldhumi=newhumi;    
    sprintf(buff, "{\"temperature\":%.1f,\"humidity\":%.0f}", temp, humi);
    client->publish(PUBTOPIC,buff);
    count10=0; //reset counter
  }
}

void loop() {
  ArduinoOTA.handle();
  client->loop(); 
  publishDHT(); //publish temp and humi if needed
}

HomeKitから使用する

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

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

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

のように設定しました。この結果、iPhoneやMacのホームには、こんな形で現れます。DHT20が測定した室温も表示されます。室温部分をクリックするとエアコンがon/offします。

   

余白をクリックすると、On/off、温度調整、冷暖房切り替えを行うウィンドウが開きます。

また、このウィンドウの歯車アイコンをクリックすると、「ファンの速さ」が0から100までのスライダで調節でき、さらに「首振り」をスイッチでon/offできます。ファンの速さは、数値が低い方から自動・しずか・1・2・3・4・5・6・7(最強) にマッピングされてます。また首振りは、垂直方向のスウィングに割り当ててあります。(水平スウィング機能は元々ありません)

まとめ

前回はパナソニックと三菱のエアコンを対象としましたが、今回はダイキンのエアコンを対象にして、スマートリモコンをDIYしました

IRremoteESP8266ライブラリは多数のメーカの基本機能に正しく対応しています。しかし一部の機能で実際のリモコンと動作が違っていましたので調整しました。またリーダー部分(赤外線パターンの冒頭の部分)の振る舞いも調整しました。

エアコンリモコンは、どのメーカー製品も、ライブラリの動作と実際のリモコン動作がどうしても異なるようです。多少違っても動作はするのですが、きっちりと合わせられると気持ちが良いです。

コメント

タイトルとURLをコピーしました