エアコンリモコンに相当するアクセサリを作ろうと思いました。ターゲットはパナソニックのエアコンです(他のメーカーでも同様に作れると思います)。下図のように、(右から)HomeKitに接続したHomebridgeサーバから、MQTT経由でコマンドを出し、ESP32で受け取って、赤外線LEDからリモコン信号を送り、エアコンをコントロールします。

前編では、この計画のうちの左半分、ESP32から赤外線パターンを送出する機能を作りました。
今回は中編として、右半分、MQTT対応部分を完成させます。

最初に、HomeKitのHeater Coolerアクセサリの機能を調べて、必要なMQTTメッセージの実装について書きます。次に、前編で作った赤外線リモコン機能にMQTTクライアントを組み込みます。
MQTTに関してはこちらをご覧ください。
前回のあらすじ
パナソニックのエアコンリモコン、A75C3777の信号を取得し、ネット上の解析情報をもとに、ビット情報を把握しました。

次に、ESP32で赤外線を送出する回路を作成しました。

次に、赤外線リモコン用のライブラリIRremoteESP8266を使い、そこに用意されているパナソニックエアコンクラスを用いて、リモコンと同一の赤外線信号を出せるようにプログラムしました。

ダミーのエアコンアクセサリを作って調査する
では今回の本編です。まずはHomeKitのHeater Coolerアクセサリがどのように動いているかを調査しました。そのためにHomebridgeにインストールしたMqttthingプラグイン
でダミーのエアコンをデフォルト設定で作ってみました。アクセサリのタイプはHeater Coolerを選びます。iPhoneやMacのホームには、こんな形で現れます。

気温の表示をクリックするとon/offします。それ以外の部分をクリックすると、On/off、温度調整、動作モード切り替え(冷房・暖房・自動)を行うウィンドウが開きます。

また、このウィンドウの歯車アイコンをクリックする(もしくは上にスクロールする)と、「ファンの速さ」が0から100までのスライダで調節でき、さらに「首振り」をスイッチでon/offできます。

運転切り替えにある「自動」は、日本のエアコンの「自動」に相当するようなモードかと思ったのですが、少し違ってました。以下のように、高低2種類の温度を設定して、それを上回る・下回る場合は、冷房・暖房を作動させるという機能でした。日本のエアコンには無い方法なので、「自動」は使わないことにします。

トピックとメッセージを調べる
次に、HomeKitのユーザ操作によって、MQTTのどのトピックにどのようなメッセージが流れるかを調べました。Mqttthingプラグインの説明によると、以下の項目に、トピック名もしくはjsonのキーを指定することになってます。
- getRotationMode
- setRotationMode
- getActive
- setActive
- getCoolingThresholdTemperature
- setCoolingThresholdTemperature
- getCurrentHeaterCoolerState
- getCurrentTemperature
- getHeatingThresholdTemperature
- setHeatingThresholdTemperature
- getRotationSpeed
- setRotationSpeed
- getSwingMode
- setSwingMode
- getTargetHeaterCoolerState
- setTargetHeaterCoolerState
- getTemperatureDisplayUnits
- setTemperatureDisplayUnits
そこで、これら全部の項目に別個のトピックを設定して、どういうメッセージが流れるのか調べてみました。ホームに現れたアクセサリを操作して、mosquitto_subコマンドで流れるメッセージをモニターしました。またgetなんとかというトピックは、ESP32の方からパブリッシュするタイプだと考えて、mosquitto_pubコマンドを使って色々なメッセージを流してみました。
その結果、全く使われないトピックも多くありました。まだ実装されていないのかもしれません。またgetのトピックにメッセージを流しても、表示に影響を与えないものも多かったです。
また、リモコン側からエアコンの状態を知ることはできません。なので、応答のしようのない項目もありました。例えば、getRotationSpeedは、単なるリモコンであるESP32からは、エアコンの風量設定をを知ることができないので、HomeKitに伝えることができません。それらを省略すると、結局は、以下の項目だけを使うことになりました。
- setActive(エアコンをon/offする)
- setCoolingThresholdTemperature(冷房温度を設定する)
- getCurrentTemperature(現在の室温を伝える)
- setHeatingThresholdTemperature(暖房温度を設定する)
- setRotationSpeed(風量を設定する)
- setTargetHeaterCoolerState(運転モードを設定する)
- setSwingMode(手元のエアコンには風向スウィング機能は無いけど、風向変更にマップし直す)
その結果、Mqttthingプラグインを以下のように設定しました。
{
"type": "heaterCooler",
"name": "Aircon",
"url": "mqtt://localhost:1883",
"username": "xxxxxxxx",
"password": "XXXXXXXX",
"topics": {
"setActive": "mqttthing/irOffice/set/Active",
"setCoolingThresholdTemperature": "mqttthing/irOffice/set/CoolingThreasholdTemperature",
"getCurrentTemperature": "mqttthing/irOffice/get.$temperature",
"setHeatingThresholdTemperature": "mqttthing/irOffice/set/HeatingThresholdTemperature",
"setRotationSpeed": "mqttthing/irOffice/set/otationSpeed",
"setTargetHeaterCoolerState": "mqttthing/irOffice/set/TargetHeaterCoolerState",
"SwingMode": "mqttthing/irOffice/set/SwingMode"
},
"restrictHeaterCoolerState": [1, 2],
"accessory": "mqttthing"
},
restrictHeaterCoolerStateは、動作を限定するオプションで、[1,2]と指定することで、「自動」モード無しで、「暖房」「冷房」のみの動作モードになります。
ユーザ操作に対応するMQTTメッセージを確認する
プログラムで、MQTTメッセージにどのように対応すべきかを確認するために、mosquitto_subコマンドでモニターしながら、エアコンアクセサリを操作しました。すると以下のようにメッセージが流れました。
% mosquitto_sub -h localhost -u xxxx -P XXXX -t mqttthing/irOffice/# -v オフにする mqttthing/irOffice/set/Active false 冷房にする mqttthing/irOffice/set/TargetHeaterCoolerState COOL mqttthing/irOffice/set/Active true 暖房にする mqttthing/irOffice/set/TargetHeaterCoolerState HEAT mqttthing/irOffice/set/Active true 暖房の状態で設定温度を20度にする(ドラッグ中はメッセージが出ない) mqttthing/irOffice/set/HeatingThresholdTemperature 20 冷房にする mqttthing/irOffice/set/TargetHeaterCoolerState COOL mqttthing/irOffice/set/Active true 冷房の状態で設定温度を28度にする(ドラッグ中はメッセージが出ない) mqttthing/irOffice/set/CoolingThresholdTemperature 28 冷房の状態でファンのスライダを0%付近から50%付近にドラッグする (ドラッグ中もメッセージが出る) mqttthing/irOffice/set/RotationSpeed 26 mqttthing/irOffice/set/Active true mqttthing/irOffice/set/RotationSpeed 35 mqttthing/irOffice/set/Active true mqttthing/irOffice/set/RotationSpeed 49 mqttthing/irOffice/set/Active true スウィングをonにする mqttthing/irOffice/set/SwingMode ENABLED スウィングをoffにする mqttthing/irOffice/set/SwingMode DISABLED
こんな感じでした。
MQTTに対応するプログラムを考える
この動作を参考にして、ESP32のプログラムを以下のように考えました。
エアコン状態を保存する変数を用意する:状態は以下
{on/off、運転モード、冷房温度、暖房温度、風量、風向のスウィング}
setup(){
エアコン状態を初期化する
set/#のトピックに対処するコールバック関数onMessageReceived()割り当てる
}
onMessageReceived(){ set/#が流れた時に呼び出される関数
もしトピックがset/RotationSpeed()ならば
{エアコン状態の風量を設定する。この後setActiveが来るので赤外線は出さない}
もしトピックがset/CoolingThresholdTemperature()ならば
{運転モードを冷房にして温度を設定する、赤外線を出す}
もしトピックがset/HeatingThresholdTemperature()ならば
{運転モードを暖房にして温度を設定する、赤外線を出す}
もしトピックがset/SwingMode()ならば
{エアコン状態の風向を設定する、赤外線を出す}
もしトピックがset/Active()ならば
{メッセージのtrue/falseに合わせて、エアコン状態をon/offにして赤外線を送出する}
}
setTargetHeaterCoolerState(){
メッセージに合わせて運転モードを設定する。この後setActiveが来るので赤外線は出さない
}
loop(){
MQTTのloop
}
プログラムは以下です。使用するトピックス名は、”mqttthing/irOffice/set/#”にしました。これに合致するトピックが来たらコールバック関数onMessageReceivedを呼び出して、その中で#の部分を切り出して判定する方式です。この関数が一番長くなってしまいました。
/* IRremoteESP8266 for Panasonic Aircon */
#include <Arduino.h>
//IR Remote
#include <IRremoteESP8266.h>
#include <ir_Panasonic.h>
const uint16_t kIrLed = 4; //GPIO for IR LED. Recommended: 4.
IRPanasonicAc pana=IRPanasonicAc(kIrLed); //instance of Panasonic AC
//MQTT
#include <EspMQTTClient.h>
EspMQTTClient *client; //instance of MQTT
//WiFi & MQTT parameters
const char SSID[] = "XXXXXXXX"; //WiFi SSID
const char PASS[] = "xxxxxxxx"; //WiFi password
char CLIENTID[] = "IRremote_9792390784"; //something random
const char MQTTADD[] = "192.168.xxx.xxx"; //Broker IP address
const short MQTTPORT = 1883; //Broker port
const char MQTTUSER[] = "xxxx";//Can be omitted if not needed
const char MQTTPASS[] = "XXXX";//Can be omitted if not needed
const char SUBTOPIC[] = "mqttthing/irOffice/set/#"; //to subscribe commands
const char PUBTOPIC[] = "mqttthing/irOffice/get"; //to publish temperature
const char DEBUG[] = "mqttthing/irOffice/debug"; //topic for debug
//base value for the IR Remote state (Panasonic AC)
const uint8_t kPanasonicA75CState[kPanasonicAcStateLength] = {
0x02, 0x20, 0xE0, 0x04, 0x00, 0x00, 0x00, 0x06, 0x02,
0x20, 0xE0, 0x04, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00,
0x00, 0x06, 0x60, 0x00, 0x00, 0x80, 0x00, 0x06, 0x00};
void setup() {
dht.begin(GPIO_SDA, GPIO_SCL); //DHT20
pana.begin(); //IR Remo
pana.setRaw(kPanasonicA75CState);
pana.setTemp(27);
pana.setMode(kPanasonicAcCool);
client = new EspMQTTClient(SSID,PASS,MQTTADD,MQTTUSER,MQTTPASS,CLIENTID,MQTTPORT);
delay(1000);
}
//MQTT: subtopic call back
void onMessageReceived(const String& topic, const String& message) {
String command = topic.substring(topic.lastIndexOf("/") + 1);
if (command.equals("Active")) {
if(message.equalsIgnoreCase("true")) pana.on();
if(message.equalsIgnoreCase("false")) pana.off();
pana.send();
}else if(command.equals("TargetHeaterCoolerState")){
if(message.equalsIgnoreCase("COOL")) {
switchMode(kPanasonicAcCool);
}
if(message.equalsIgnoreCase("HEAT")) {
switchMode(kPanasonicAcHeat);
}
}else if(command.equals("CoolingThresholdTemperature")){
switchMode(kPanasonicAcCool);//just in case
pana.setTemp(message.toInt());
pana.send();
}else if(command.equals("HeatingThresholdTemperature")){
switchMode(kPanasonicAcHeat);//just in case
pana.setTemp(message.toInt());
pana.send();
}else if(command.equals("SwingMode")){
if(message.equalsIgnoreCase("DISABLED")) pana.setSwingVertical(kPanasonicAcSwingVLowest);
if(message.equalsIgnoreCase("ENABLED")) pana.setSwingVertical(kPanasonicAcSwingVAuto);
pana.send();
}else if(command.equals("RotationSpeed")){
int speed=message.toInt();
if(speed > 5) pana.setFan(kPanasonicAcFanAuto);
else if(speed > 20) pana.setFan(kPanasonicAcFanMin);
else if(speed > 40) pana.setFan(kPanasonicAcFanLow);
else if(speed > 60) pana.setFan(kPanasonicAcFanMed);
else if(speed > 80) pana.setFan(kPanasonicAcFanHigh);
else pana.setFan(kPanasonicAcFanMax);
}
}
//MQTT: connection callback
void onConnectionEstablished() {
client->subscribe(SUBTOPIC, onMessageReceived); //set callback
}
//IR Remo: recover the temperature, fan, and swing on mode switch.
void switchMode(const uint8_t targetmode){
static uint8_t cTemp=27, hTemp=20;
static uint8_t cFan=kPanasonicAcFanAuto, hFan=kPanasonicAcFanAuto;
static uint8_t cSwingV=0xF, hSwingV=0xF;
uint8_t currentmode;
currentmode = pana.getMode();
if(targetmode == currentmode) return; //do nothing
if(targetmode == kPanasonicAcCool) {
hTemp=pana.getTemp(); hFan=pana.getFan(); hSwingV=pana.getSwingVertical();
pana.setMode(targetmode);
pana.setTemp(cTemp);pana.setFan(cFan);pana.setSwingVertical(cSwingV);
}
if(targetmode == kPanasonicAcHeat) {
cTemp=pana.getTemp(); cFan=pana.getFan(); cSwingV=pana.getSwingVertical();
pana.setMode(targetmode);
pana.setTemp(hTemp);pana.setFan(hFan);pana.setSwingVertical(hSwingV);
}
}
void loop() {
client->loop();
}
プログラムは長くなってしまいましたが、MQTTトピックとメッセージに従って、前回の記事で紹介したエアコンクラスのメソッドを呼び出しているだけです。使っているメソッドは、(インスタンス名をpanaにしているので)
- pana.on();
- pana.off();
- pana.send();
- pana.setTemp();
- pana.setSwingVertical();
- pana.setFan();
- pana.setMode()
などです。また、運転モードを切り替える時に、設定温度などをバックアップしておく仕組みも前回の記事のまま使ってます。
次回の予定
MQTTトピックの、get/CurrentTemperatureは、リモコンが、現在の室温をHomeKitに知らせる時に使います。室温は、エアコンがオフの時に表示されます。今回はこれが未実装ですので、エアコンがオフの時に0度と表示されてしまいます。

次回は、ESP32に温度センサ(DHT20)をつけて、正しい室温を送ることにします。こちらで作った仕組みです。
また、実際に使用してみたところ、赤外線が少し弱いようです。今は赤外線LEDに37mA程度流していますが、LEDにもFETにも余裕があるので、もう少し流しても良いです。また、赤外線LEDの指向性が強いため(半減角度が20度)、LEDがエアコン方向に向くように調整する必要があります。LEDの数を増やして、指向性を広げると良いと考えています。赤外線LED周りの改良も、次回の後編で予定してます。
まとめ
ESP32からエアコンリモコンするプログラムを作った前編に引き続き、本編ではMQTTメッセージを受けて赤外線信号を出すようにプログラムを追記しました。これで、iPhoneやMacのホーム.appからHeater Coolerアクセサリとしてコントロールできるようになりました。これでエアコン操作はできましたので、一応の完成です。次回は、室温を伝える部分を作り、赤外線LEDの改良を行う予定です。
以下に続きます:






コメント