We had some very (very!) hot days here in Sydney, up to 40 DegC over several days. After a day in the office, you open the door to your apartment and… 50 DegC 🙁 I looked for a solution to cool down home right after i left the office – with my phone – using MQTT 😉

Hardware

I am using three components for my project. The MCU, an adapter board and the IR-LED

NodeMCU ESP8266

4Mbyte ROM

On-board USB/UART Grove Base Shield for NodeMCU 5 Digital connectors

2 I2C sockets

1 Analog connectors (A0)

Power indicator LED Shop | Wiki Grove – Infrared Emitter Voltage: 3.3-5V

Distance:10m Shop | Wiki

Software

The Basics

The project is based on the Arduino core for ESP8266 and uses several additional libraries. I am using the platform.io IDE for development. The advantages are the fully integrated build and deployment process (via USB or over WiFi), the library manager and the code completion and linting. You can download my complete platform.io project here.

You’ll also need an MQTT server like RabbitMQ to send commands and receive the status. If you are familiar with Docker, you should read my previous post Dockerize RabbitMQ to get a server up and running in no time. If not: There are several binary pages on the RabbitMQ Website.

Basic structure:

Connection to the WiFi Managing OTA updates Connection to the MQTT Server Subscribing to a command channel

Emitting current state to a status channel Emitting IR commands to the A/C

HeatpumpIR Library

The HeatpumpIR library is the heart of the project. Its designed to expose a unified interface for several different A/C manufacturers like Daikin, Samsung, Panasonic and other. The library is compatible to Arduino and ESP8266 MCUs.

The usage is really simple, have a look at the example code

// Include the appropriate header for your A/C #include <DaikinHeatpumpIR.h> // Define a ir sender at port D2 IRSenderBitBang irSender(D2); // Define the heatpump DaikinHeatpumpIR *heatpump; void setup() { // ... } void main() { // Send the command heatpump->send(irSender, POWER_ON, MODE_COOL, FAN_AUTO, 24, VDIR_AUTO, HDIR_AUTO); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // Include the appropriate header for your A/C #include <DaikinHeatpumpIR.h> // Define a ir sender at port D2 IRSenderBitBang irSender ( D2 ) ; // Define the heatpump DaikinHeatpumpIR * heatpump ; void setup ( ) { // ... } void main ( ) { // Send the command heatpump -> send ( irSender , POWER_ON , MODE_COOL , FAN_AUTO , 24 , VDIR_AUTO , HDIR_AUTO ) ; }

This command will power on the A/C, set the mode cooling, the fan to auto, 24 degC and both swings to auto.

You can find a list of possible values here.

If you don’t have a Daikin A/C, have a look into the library and search for your brand. Just change the #include and you should be good to go.

The complete code

Depending on your IDE you have to search for the libraries by yourself. You’ll need PubSubClient, HeatpumpIR and Timer.

Again: It’s easier to install the Platform.io IDE and use my package 🙂

In both cases you have to modify several defines on top of the main.ino, here are the most relevant:

Name Description IR_SEND_PIN The PIN where the IR LED is connected WIFI_SSID Your WiFi network name WIFI_PASS WiFi password MQTT_HOST Mqtt hostname like mqtt.mydomain.com MQTT_USER MQTT Username MQTT_PASS MQTT Password

main.ino #include <ESP8266WiFi.h> #include <ESP8266mDNS.h> #include <WiFiUdp.h> #include <ArduinoOTA.h> #include <PubSubClient.h> // MQTT #include <DaikinHeatpumpIR.h> #include "Timer.h" #define TOPSZ 60 // Max number of characters in topic string #define MESSZ 240 // Max number of characters in JSON message string #define IR_SEND_PIN 12 #define WIFI_SSID "mysecretwifi" #define WIFI_PASS "superpassword" #define MQTT_HOST "mymqtthost.com" #define MQTT_PORT 1883 #define MQTT_USER "mqttuser" #define MQTT_PASS "mqttpassword" #define MQTT_CLIENT_ID "daikinremote" #define MQTT_STATUS_CHANNEL "stat/daikin" #define MQTT_COMMAND_CHANNEL "cmnd/daikin/#" // dont miss the # at the end! WiFiClient espClient; // Wifi Client PubSubClient mqttClient(espClient); // MQTT Client IRSenderBitBang irSender(IR_SEND_PIN); DaikinHeatpumpIR *heatpump; Timer t; // Set defaults uint8_t AC_POWER = POWER_OFF, AC_MODE = MODE_AUTO, AC_FAN = FAN_AUTO, AC_TEMP = 24, AC_VSWING = VDIR_AUTO, AC_HSWING = HDIR_AUTO; void setup() { Serial.begin(115200); delay(10); Serial.println("Booting"); initWIFI(); initOTA(); Serial.println("Ready"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); // Init loop timers t.every(5000, fiveLoop); t.every(10000, tenLoop); heatpump = new DaikinHeatpumpIR(); initMQTT(); } void loop() { ArduinoOTA.handle(); mqttClient.loop(); t.update(); } void tenLoop() { Serial.println("Ping..."); publishState(); } // Reconnect to mqtt every 5 seconds if connection is lost void fiveLoop() { if (!mqttClient.connected()) { reconnectMQTT(); } } void initMQTT() { mqttClient.setServer(MQTT_HOST, MQTT_PORT); if (!mqttClient.connected()) { reconnectMQTT(); } publishState(); } void mqttDataCb(char* topic, byte* data, unsigned int data_len) { char svalue[MESSZ]; char topicBuf[TOPSZ]; char dataBuf[data_len+1]; strncpy(topicBuf, topic, sizeof(topicBuf)); memcpy(dataBuf, data, sizeof(dataBuf)); dataBuf[sizeof(dataBuf)-1] = 0; snprintf_P(svalue, sizeof(svalue), PSTR("RSLT: Receive topic %s, data size %d, data %s"), topicBuf, data_len, dataBuf); Serial.println(svalue); // Extract command memmove(topicBuf, topicBuf+sizeof(MQTT_COMMAND_CHANNEL)-2, sizeof(topicBuf)-sizeof(MQTT_COMMAND_CHANNEL)); int16_t payload = atoi(dataBuf); // -32766 - 32767 if (!strcmp(topicBuf, "power")) { Serial.print("power "); Serial.println(payload); AC_POWER = payload; } else if (!strcmp(topicBuf, "mode")) { Serial.print("mode "); Serial.println(payload); AC_MODE = payload; } else if (!strcmp(topicBuf, "fan")) { Serial.print("fan "); Serial.println(payload); AC_FAN = payload; } else if (!strcmp(topicBuf, "temp")) { Serial.print("temp "); Serial.println(payload); AC_TEMP = payload; } heatpump->send(irSender, AC_POWER, AC_MODE, AC_FAN, AC_TEMP, AC_VSWING, AC_HSWING); publishState(); } void publishState() { char message[MESSZ]; sprintf( message, "{\"power\":%d,\"mode\":%d, \"fan\":%d,\"temp\":%d,\"vswing\":%d,\"hswing\":%d}", AC_POWER, AC_MODE, AC_FAN, AC_TEMP, AC_VSWING, AC_HSWING ); mqttClient.publish(MQTT_STATUS_CHANNEL, message, true); } void reconnectMQTT() { // Loop until we're reconnected Serial.println("Attempting MQTT connection..."); // Attempt to connect if (mqttClient.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS)) { Serial.println("connected"); mqttClient.setCallback(mqttDataCb); mqttClient.subscribe(MQTT_COMMAND_CHANNEL); } else { Serial.print("failed, rc="); Serial.println(mqttClient.state()); } } void initOTA() { // Port defaults to 8266 // ArduinoOTA.setPort(8266); // Hostname defaults to esp8266-[ChipID] // ArduinoOTA.setHostname("myesp8266"); // No authentication by default // ArduinoOTA.setPassword("admin"); // Password can be set with it's md5 value as well // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); ArduinoOTA.onStart([]() { String type; if (ArduinoOTA.getCommand() == U_FLASH) type = "sketch"; else // U_SPIFFS type = "filesystem"; // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() Serial.println("Start updating " + type); }); ArduinoOTA.onEnd([]() { Serial.println("

End"); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { Serial.printf("Progress: %u%%\r", (progress / (total / 100))); }); ArduinoOTA.onError([](ota_error_t error) { Serial.printf("Error[%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); else if (error == OTA_END_ERROR) Serial.println("End Failed"); }); ArduinoOTA.begin(); } void initWIFI() { WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.waitForConnectResult() != WL_CONNECTED) { Serial.println("Connection Failed! Rebooting..."); delay(5000); ESP.restart(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 #include <ESP8266WiFi.h> #include <ESP8266mDNS.h> #include <WiFiUdp.h> #include <ArduinoOTA.h> #include <PubSubClient.h> // MQTT #include <DaikinHeatpumpIR.h> #include "Timer.h" #define TOPSZ 60 // Max number of characters in topic string #define MESSZ 240 // Max number of characters in JSON message string #define IR_SEND_PIN 12 #define WIFI_SSID "mysecretwifi" #define WIFI_PASS "superpassword" #define MQTT_HOST "mymqtthost.com" #define MQTT_PORT 1883 #define MQTT_USER "mqttuser" #define MQTT_PASS "mqttpassword" #define MQTT_CLIENT_ID "daikinremote" #define MQTT_STATUS_CHANNEL "stat/daikin" #define MQTT_COMMAND_CHANNEL "cmnd/daikin/#" // dont miss the # at the end! WiFiClient espClient ; // Wifi Client PubSubClient mqttClient ( espClient ) ; // MQTT Client IRSenderBitBang irSender ( IR_SEND_PIN ) ; DaikinHeatpumpIR * heatpump ; Timer t ; // Set defaults uint8 _ t AC_POWER = POWER_OFF , AC_MODE = MODE_AUTO , AC_FAN = FAN_AUTO , AC_TEMP = 24 , AC_VSWING = VDIR_AUTO , AC_HSWING = HDIR_AUTO ; void setup ( ) { Serial . begin ( 115200 ) ; delay ( 10 ) ; Serial . println ( "Booting" ) ; initWIFI ( ) ; initOTA ( ) ; Serial . println ( "Ready" ) ; Serial . print ( "IP address: " ) ; Serial . println ( WiFi . localIP ( ) ) ; // Init loop timers t . every ( 5000 , fiveLoop ) ; t . every ( 10000 , tenLoop ) ; heatpump = new DaikinHeatpumpIR ( ) ; initMQTT ( ) ; } void loop ( ) { ArduinoOTA . handle ( ) ; mqttClient . loop ( ) ; t . update ( ) ; } void tenLoop ( ) { Serial . println ( "Ping..." ) ; publishState ( ) ; } // Reconnect to mqtt every 5 seconds if connection is lost void fiveLoop ( ) { if ( ! mqttClient . connected ( ) ) { reconnectMQTT ( ) ; } } void initMQTT ( ) { mqttClient . setServer ( MQTT_HOST , MQTT_PORT ) ; if ( ! mqttClient . connected ( ) ) { reconnectMQTT ( ) ; } publishState ( ) ; } void mqttDataCb ( char * topic , byte * data , unsigned int data_len ) { char svalue [ MESSZ ] ; char topicBuf [ TOPSZ ] ; char dataBuf [ data_len + 1 ] ; strncpy ( topicBuf , topic , sizeof ( topicBuf ) ) ; memcpy ( dataBuf , data , sizeof ( dataBuf ) ) ; dataBuf [ sizeof ( dataBuf ) - 1 ] = 0 ; snprintf_P ( svalue , sizeof ( svalue ) , PSTR ( "RSLT: Receive topic %s, data size %d, data %s" ) , topicBuf , data_len , dataBuf ) ; Serial . println ( svalue ) ; // Extract command memmove ( topicBuf , topicBuf + sizeof ( MQTT_COMMAND_CHANNEL ) - 2 , sizeof ( topicBuf ) - sizeof ( MQTT_COMMAND_CHANNEL ) ) ; int16 _ t payload = atoi ( dataBuf ) ; // -32766 - 32767 if ( ! strcmp ( topicBuf , "power" ) ) { Serial . print ( "power " ) ; Serial . println ( payload ) ; AC_POWER = payload ; } else if ( ! strcmp ( topicBuf , "mode" ) ) { Serial . print ( "mode " ) ; Serial . println ( payload ) ; AC_MODE = payload ; } else if ( ! strcmp ( topicBuf , "fan" ) ) { Serial . print ( "fan " ) ; Serial . println ( payload ) ; AC_FAN = payload ; } else if ( ! strcmp ( topicBuf , "temp" ) ) { Serial . print ( "temp " ) ; Serial . println ( payload ) ; AC_TEMP = payload ; } heatpump -> send ( irSender , AC_POWER , AC_MODE , AC_FAN , AC_TEMP , AC_VSWING , AC_HSWING ) ; publishState ( ) ; } void publishState ( ) { char message [ MESSZ ] ; sprintf ( message , "{\"power\":%d,\"mode\":%d, \"fan\":%d,\"temp\":%d,\"vswing\":%d,\"hswing\":%d}" , AC_POWER , AC_MODE , AC_FAN , AC_TEMP , AC_VSWING , AC _ HSWING ) ; mqttClient . publish ( MQTT_STATUS_CHANNEL , message , true ) ; } void reconnectMQTT ( ) { // Loop until we're reconnected Serial . println ( "Attempting MQTT connection..." ) ; // Attempt to connect if ( mqttClient . connect ( MQTT_CLIENT_ID , MQTT_USER , MQTT_PASS ) ) { Serial . println ( "connected" ) ; mqttClient . setCallback ( mqttDataCb ) ; mqttClient . subscribe ( MQTT_COMMAND_CHANNEL ) ; } else { Serial . print ( "failed, rc=" ) ; Serial . println ( mqttClient . state ( ) ) ; } } void initOTA ( ) { // Port defaults to 8266 // ArduinoOTA.setPort(8266); // Hostname defaults to esp8266-[ChipID] // ArduinoOTA.setHostname("myesp8266"); // No authentication by default // ArduinoOTA.setPassword("admin"); // Password can be set with it's md5 value as well // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); ArduinoOTA . onStart ( [ ] ( ) { String type ; if ( ArduinoOTA . getCommand ( ) == U_FLASH ) type = "sketch" ; else // U_SPIFFS type = "filesystem" ; // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() Serial . println ( "Start updating " + type ) ; } ) ; ArduinoOTA . onEnd ( [ ] ( ) { Serial . println ( "

End" ) ; } ) ; ArduinoOTA . onProgress ( [ ] ( unsigned int progress , unsigned int total ) { Serial . printf ( "Progress: %u%%\r" , ( progress / ( total / 100 ) ) ) ; } ) ; ArduinoOTA . onError ( [ ] ( ota_error _ t error ) { Serial . printf ( "Error[%u]: " , error ) ; if ( error == OTA_AUTH_ERROR ) Serial . println ( "Auth Failed" ) ; else if ( error == OTA_BEGIN_ERROR ) Serial . println ( "Begin Failed" ) ; else if ( error == OTA_CONNECT_ERROR ) Serial . println ( "Connect Failed" ) ; else if ( error == OTA_RECEIVE_ERROR ) Serial . println ( "Receive Failed" ) ; else if ( error == OTA_END_ERROR ) Serial . println ( "End Failed" ) ; } ) ; ArduinoOTA . begin ( ) ; } void initWIFI ( ) { WiFi . mode ( WIFI_STA ) ; WiFi . begin ( WIFI_SSID , WIFI_PASS ) ; while ( WiFi . waitForConnectResult ( ) != WL_CONNECTED ) { Serial . println ( "Connection Failed! Rebooting..." ) ; delay ( 5000 ) ; ESP . restart ( ) ; } }

Example usage

Software uploaded, Hardware connected, WiFi and MQTT connection ok? Good! 🙂 Time to send the first command via MQTT. On MacOS i am using mqtt.fx to connect to my MQTT server. You can of course use another tool, it’s up to you to find and configure them 😉

The software is listening for commands on a specific topic configured via the constant MQTT_COMMAND_CHANNEL. The default configuration is “ cmnd/daikin/# “. To set the power on the A/C, you just publish “1” to the topic “ cmnd/daikin/power “. The MCU will now send the command via IR and post the current values of every setting (power, mode, fan etc.) as JSON to the topic “ stat/daikin ”

{"power":1,"mode":3, "fan":2,"temp":23,"vswing":0,"hswing":0}

Command overview and value mapping

Command Descr → Value cmnd/daikin/power POWER_OFF → 0

POWER_ON → 1 cmnd/daikin/mode MODE_AUTO → 1

MODE_HEAT → 2

MODE_COOL → 3

MODE_DRY → 4

MODE_FAN → 5 cmnd/daikin/fan FAN_AUTO → 0

FAN_1 → 1

FAN_2 → 2

FAN_3 → 3

FAN_4 → 4

FAN_5 → 5 cmnd/daikin/temp Numeric value ♦ VDIR_AUTO → 0 ♦ HDIR_AUTO → 0

♦ The swing functionality is not implemented for now – feel free to do it by yourself 😉

Android App anyone?

I’m using a nice Android app called MQTT Dash