ESP32 Home Automation

  • Author: Valentin Radu
  • Email: valentin.radu@valinet.ro
  • Master: SRIC

Introduction

This project implements two functionalities for my smart home setup:

  • Speaker volume control using TV remote Demo
  • Smart Air Conditioning Demo

Both tasks are centered around an ESP32-WROOM board.

Speaker Volume Control using TV remote

The goal for this task was to control the volume for the stereo system using the TV's remote control.

About

The speakers are connected using the TV's headphone jack. Unfortunately, the software on my TV model is particularly dumb, in that it only allows changing the volume of its internal speakers using the volume controls on the remote. Initially, I wanted to use optical audio, but that does not allow for volume control due to its nature (optical audio transmits digital audio over infrared which simply carries the soundwave without any volume information - traditionally, it is the job of the receiver unit to regulate the volume). Then, I looked into using the HDMI ARC (audio return channel) feature, but, again, this is poorly implemented and does not allow for volume change. Lastly, I chose to connect the speakers via the headphone jack, which allows change via the “Sound” section of the TV menu, but not directly using the IR remote control.

Out of sheer luck, while researching something totally unrelated, I found out that the Sharp 42LE756EN TV that I have is actually made by Vestel . It is an old TV, but it has a 3D panel, which are basically impossible to buy nowadays, so that's why I keep it around. Anyway, Vestel is a pretty large enterprise and they also sell panels for usage in industrial/commercial signage for example. In those kinds of applications, the panels are not controlled via an IR remote control, but rather using some protocol over some sort of physical link, like serial or Ethernet. Turns out, Vestel actually ships similar software on all of their panels and just skin it according to the service are of the product; thus, their consumer LCDs still have the “customer control” implemented, which allows controlling the TV functionality via Ethernet (or an RS-232 header which I do not really have access to and an easy way to interface with). The “customer control” protocol is a VERY simple protocol: connect (telnet) via port 1986, and then send some commands in plain text and that's it. The commands required are:

  • VOLUME 0 for muting the internal speakers
  • HEADPHONEVOLUME x for setting the volume level of the headphone port to x, where x is from 0 to 63 according to the UI in the TV's menu

The protocol described in the PDF is not totally accurate to what is experienced on my TV unit: there is no reply back, but the command actually works, it does actually change the output volume, which is great.

Hardware

So, for this task, I have decided to use and ESP32 board to which I connected a TSOP4838 IR receiver I have taken out from some smart IR controlled light bulb that I had no use for (I was experiencing a shortage of IR receivers in my parts inventory). This receives data from my TV's remote control. Everything else that is required is already embedded on the ESP32 board.

Pinout

  • TSOP4838 pin 1 > ESP32 pin 14
  • TSOP4838 pin 2 > ESP32 5V pin
  • TSOP4838 pin 3 > ESP32 GND pin

Software

The board is connected to my home network. The IR sensor receives codes from my TV's remote control - when the codes for the volume buttons are received, a connection to the TV is made over the network (the TV is connected as well to my home network using an Ethernet cable and a static local IP address) and the two commands described above are sent to the TV: the internal speakers are muted and the desired headphone volume level is set in the TV.

Gotchas

Changing the headphone volume produces no UI on the TV. I have experimented with the OSD_PRINT command in the protocol which adds text in the top left corner of the TV image, but once the command is used, the TV crashes and reboots approximately 2 minutes after the command is used. The only UI produced corresponds to the the VOLUME 0 command, which displays the volume bar for the internal speakers with the volume set to 0. This is just aesthetic, the end result is that the headphone volume does change, so the actual sound we hear gets louder/softer, only that the change is not reflected via any UI on the screen .

As I said, not all features described in the protocol work on my TV. For example, there is a “GETHEADPHONEVOLUME” command which is supposed to return the current level for the headphone volume, but unfortunately it doesn't work on my model. This kind of is required when the ESP32 board cold starts - it would fetch the current headphone volume of the TV and start from there. As it is at the moment, I reset the volume to a sane default (16), which is an acceptable compromise. This scenario only happens after a power outage, which is VERY rare. Otherise, the ESP32 board runs continously even when the TV is off, so it “remembers” what the volume level was when the TV closed and so does the TV when it turns back on.

Another workaround for this minor inconvenience would be to memorize the current headphone level in ESP32's EEPROM and restore it when the TV is started fresh. I have decided to not implement this because of the following reasons:

  • Fear of wearing out the EEPROM cells - volume is changed quite a lot when operating the TV, and each change would basically mean a write to the EEPROM.
  • It is actually kind of hard to determine when the TV comes back “online” (when it is turned on). The ESP32 would basically have to continously attempt to connect to that address and port every x seconds, for example. When finally a connection is possible (that means, the user has turned back the TV on), reset the volume to the previous setting.

I think that such feature is not really that necessary, helping in only a fraction of the use cases, and for the considerations above I decided that I can live without it.

Here is a sample of the data received from my TV remote when holding down the volume buttons:

Timestamp : 000113.193
Library   : v2.8.6

Protocol  : SHARP
Code      : 0x40A2 (15 Bits)
uint16_t rawData[95] = {336, 1664,  346, 704,  316, 728,  344, 706,  316, 706,  318, 704,  318, 706,  316, 1708,  314, 680,  344, 1706,  316, 706,  318, 706,  314, 708,  316, 1768,  316, 698,  316, 47580,  340, 1682,  340, 706,  316, 708,  316, 706,  316, 706,  292, 1732,  300, 1724,  312, 714,  310, 1712,  292, 732,  296, 1724,  318, 1680,  344, 1704,  294, 730,  316, 1708,  292, 43308,  338, 1680,  344, 732,  292, 708,  342, 696,  294, 730,  292, 734,  290, 732,  314, 1706,  316, 710,  292, 1706,  342, 704,  294, 730,  292, 730,  290, 1732,  292, 862,  316};  // SHARP 40A2
uint32_t address = 0x1;
uint32_t command = 0x14;
uint64_t data = 0x40A2;


Timestamp : 000114.055
Library   : v2.8.6

Protocol  : SHARP
Code      : 0x42A2 (15 Bits)
uint16_t rawData[95] = {338, 1670,  342, 704,  318, 706,  318, 704,  318, 706,  314, 1706,  318, 706,  318, 1714,  314, 708,  318, 1694,  320, 702,  318, 682,  340, 706,  316, 1706,  318, 708,  318, 46674,  364, 1682,  318, 708,  318, 706,  320, 694,  316, 706,  318, 706,  316, 1706,  318, 706,  316, 1706,  318, 706,  316, 1706,  318, 1706,  316, 1708,  316, 708,  316, 1706,  320, 44288,  342, 1678,  344, 680,  344, 706,  318, 706,  320, 678,  342, 1706,  316, 712,  316, 1698,  316, 706,  316, 1710,  316, 706,  316, 708,  316, 706,  318, 1704,  318, 684,  342};  // SHARP 42A2
uint32_t address = 0x1;
uint32_t command = 0x15;
uint64_t data = 0x42A2;


Timestamp : 000116.245
Library   : v2.8.6

Protocol  : SHARP
Code      : 0x43A2 (15 Bits)
uint16_t rawData[95] = {336, 1692,  318, 708,  314, 706,  292, 730,  292, 732,  298, 1724,  318, 1682,  342, 1706,  316, 706,  316, 1710,  290, 732,  316, 710,  300, 722,  294, 1730,  292, 732,  290, 45722,  316, 1702,  320, 706,  308, 716,  314, 708,  316, 684,  342, 708,  290, 734,  290, 732,  316, 1710,  314, 708,  314, 1706,  318, 1704,  318, 1706,  318, 706,  318, 1696,  294, 45432,  314, 1680,  344, 706,  292, 730,  316, 706,  292, 732,  292, 1708,  340, 1706,  318, 1706,  316, 694,  292, 1734,  290, 732,  318, 706,  316, 710,  290, 1732,  292, 732,  314};  // SHARP 43A2
uint32_t address = 0x1;
uint32_t command = 0x17;
uint64_t data = 0x43A2;


Timestamp : 000332.349
WARNING: IR code is too big for buffer (>= 128). This result shouldn't be trusted until this is resolved. Edit & increase `kCaptureBufferSize`.
Library   : v2.8.6

Protocol  : SHARP
Code      : 0x40A2 (15 Bits)
uint16_t rawData[127] = {338, 1694,  320, 702,  322, 702,  320, 704,  318, 706,  318, 704,  318, 728,  318, 1708,  318, 704,  318, 1706,  318, 702,  322, 706,  318, 702,  318, 1704,  318, 704,  318, 47734,  340, 1668,  320, 704,  320, 702,  320, 702,  320, 704,  320, 1702,  320, 1702,  320, 704,  318, 1706,  320, 702,  320, 1778,  318, 1692,  322, 1704,  322, 702,  320, 1702,  320, 43230,  366, 1654,  344, 702,  320, 704,  320, 704,  318, 704,  322, 702,  320, 706,  318, 1680,  346, 704,  318, 1702,  320, 704,  320, 706,  318, 704,  318, 1706,  318, 704,  320, 47310,  342, 1678,  346, 704,  318, 704,  320, 702,  318, 702,  322, 1702,  320, 1704,  320, 704,  318, 1706,  320, 704,  318, 1710,  322, 1696,  320, 1704,  318, 704,  320, 1702,  322};  // SHARP 40A2
uint32_t address = 0x1;
uint32_t command = 0x14;
uint64_t data = 0x40A2;


Timestamp : 000332.617
WARNING: IR code is too big for buffer (>= 128). This result shouldn't be trusted until this is resolved. Edit & increase `kCaptureBufferSize`.
Library   : v2.8.6

Protocol  : UNKNOWN
Code      : 0x815AFE67 (64 Bits)
uint16_t rawData[127] = {1702, 318,  704, 320,  704, 320,  702, 320,  704, 318,  704, 320,  704, 318,  1704, 320,  828, 320,  1706, 318,  704, 318,  704, 318,  704, 320,  1704, 320,  702, 320,  47182, 324,  1696, 322,  692, 320,  706, 318,  704, 320,  704, 320,  1704, 320,  1702, 320,  704, 320,  1702, 320,  704, 320,  1702, 320,  1786, 318,  1692, 320,  704, 320,  1680, 344,  43222, 340,  1700, 322,  704, 318,  706, 318,  702, 322,  704, 318,  704, 320,  704, 320,  1706, 318,  706, 320,  1836, 320,  704, 318,  704, 320,  704, 320,  1704, 318,  708, 318,  47146, 344,  1676, 346,  704, 320,  692, 320,  706, 318,  704, 320,  1702, 320,  1704, 322,  704, 316,  1704, 320,  704, 322,  1678, 344,  1706, 320,  1702, 322,  702, 320,  1704, 318,  43314};  // UNKNOWN 815AFE67


Timestamp : 000332.889
WARNING: IR code is too big for buffer (>= 128). This result shouldn't be trusted until this is resolved. Edit & increase `kCaptureBufferSize`.
Library   : v2.8.6

Protocol  : UNKNOWN
Code      : 0x179D1856 (64 Bits)
uint16_t rawData[127] = {342, 702,  320, 704,  318, 704,  318, 704,  318, 706,  318, 704,  320, 1704,  318, 704,  320, 1704,  320, 704,  320, 704,  320, 704,  318, 1702,  320, 704,  320, 47282,  364, 1652,  348, 702,  320, 706,  318, 696,  318, 704,  320, 1704,  320, 1704,  320, 704,  320, 1702,  320, 704,  318, 1704,  320, 1704,  320, 1704,  320, 798,  320, 1704,  318, 43198,  338, 1702,  322, 702,  320, 704,  318, 704,  322, 702,  318, 704,  320, 702,  322, 1700,  322, 702,  320, 1706,  322, 702,  322, 702,  320, 704,  320, 1702,  320, 704,  318, 47278,  366, 1678,  322, 702,  322, 704,  318, 702,  322, 706,  322, 1704,  322, 1704,  318, 702,  322, 1702,  318, 704,  322, 1702,  322, 1702,  322, 1702,  320, 706,  320, 1792,  322, 43488,  340};  // UNKNOWN 179D1856


Timestamp : 000333.155
WARNING: IR code is too big for buffer (>= 128). This result shouldn't be trusted until this is resolved. Edit & increase `kCaptureBufferSize`.
Library   : v2.8.6

Protocol  : UNKNOWN
Code      : 0xB91B7E67 (64 Bits)
uint16_t rawData[127] = {706, 318,  702, 320,  702, 320,  704, 318,  702, 320,  704, 320,  1704, 320,  704, 324,  1686, 322,  704, 320,  692, 322,  702, 320,  1702, 322,  704, 320,  47022, 366,  1654, 346,  702, 322,  678, 344,  704, 318,  704, 320,  1722, 320,  1704, 320,  704, 320,  1704, 318,  704, 320,  1702, 320,  1704, 320,  1702, 320,  704, 320,  1704, 320,  43286, 338,  1680, 322,  702, 322,  702, 322,  700, 322,  704, 320,  702, 320,  702, 322,  1702, 320,  702, 322,  1700, 322,  702, 322,  718, 322,  704, 320,  1704, 320,  704, 320,  47264, 364,  1678, 322,  702, 318,  704, 318,  704, 322,  702, 320,  1704, 320,  1732, 322,  702, 322,  1700, 322,  702, 320,  1702, 320,  1702, 322,  1702, 320,  702, 322,  1704, 318,  43318, 364,  1678};  // UNKNOWN B91B7E67


Timestamp : 000333.425
WARNING: IR code is too big for buffer (>= 128). This result shouldn't be trusted until this is resolved. Edit & increase `kCaptureBufferSize`.
Library   : v2.8.6

Protocol  : UNKNOWN
Code      : 0xA39865A5 (64 Bits)
uint16_t rawData[127] = {318, 704,  318, 704,  320, 704,  320, 702,  320, 702,  322, 1702,  320, 702,  322, 1702,  320, 704,  320, 704,  320, 704,  322, 1838,  320, 678,  344, 47166,  366, 1652,  346, 702,  320, 704,  320, 704,  318, 704,  320, 1704,  320, 1692,  318, 704,  320, 1702,  322, 704,  320, 1702,  320, 1702,  322, 1680,  342, 704,  322, 1702,  320, 43318,  340, 1680,  322, 704,  318, 704,  320, 706,  318, 702,  318, 706,  318, 702,  320, 1702,  322, 702,  322, 1702,  318, 704,  322, 702,  318, 706,  320, 1706,  320, 704,  318, 47280,  362, 1658,  344, 702,  320, 702,  322, 702,  320, 702,  320, 1702,  320, 1702,  322, 704,  320, 1702,  320, 704,  320, 1702,  320, 1704,  318, 1704,  322, 702,  320, 1702,  322, 43280,  340, 1704,  322};  // UNKNOWN A39865A5


Timestamp : 000333.624
Library   : v2.8.6

Protocol  : UNKNOWN
Code      : 0xB032D22A (45 Bits)
uint16_t rawData[90] = {704, 318,  706, 318,  704, 320,  702, 320,  706, 318,  1704, 320,  704, 320,  1704, 320,  704, 320,  702, 320,  702, 322,  1702, 320,  846, 318,  47160, 318,  1702, 322,  678, 346,  702, 320,  704, 320,  704, 320,  1702, 320,  1680, 344,  702, 320,  1692, 320,  704, 320,  1704, 320,  1704, 320,  1702, 320,  702, 320,  1704, 320,  43294, 366,  1676, 324,  704, 318,  706, 318,  704, 320,  702, 320,  704, 320,  702, 320,  1704, 318,  702, 322,  1704, 318,  706, 318,  702, 320,  702, 320,  1704, 318,  694, 320 };  // UNKNOWN B032D22A

Of particular interest is what happens when a button is held down on my TV's remote: it basically repeats the same bytes over and over again and the TV acts accordingly.

Though, the way the IR library that I used works is it has an internal buffer where it collects bytes. Only when the buffer gets completely filled or the bytes stop coming (i.e. the button on the remote is depressed), does it processes the received bytes, determines a protocol and return that to the user. Unfortunately, that is really not useful, because I wanted to be notified when the button is held down and act accordingly (i.e. raise/lower the volume more the more the user holds down the button). My solution to this was to configure a smaller internal buffer that gets filled at the rate I want to be notified about the button still being held. This works really well in practice, but as can be seen, the library is not able to properly decode the subsequent bytes. My solution for this was a bit of a hack:

  • When I receive a code for SHARP, is_held is set to 1 and I memorize the command.
  • When I receive an UNKNOWN code, if is_held is 1, I repeat the last command.
  • After 300ms since the last command, I reset is_held back to 0.

Code

Below is the complete listing for the code.ino file with comments:

#include <WiFi.h>
#include <Arduino.h>
#include <assert.h>
#include <IRrecv.h>
#include <IRremoteESP8266.h>
#include <IRac.h>
#include <IRtext.h>
#include <IRutils.h>

// Network info
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// TV IP address
IPAddress ADDR(10,1,1,100);
#define PORT 1986
WiFiClient client;

// First time, reset volume to some sane default instead of increasing/decreasing it
bool first = true;
// Current volume level, set to desired default initially
int volume = 16;
// Whether the mute button was pressed on the remote control
bool muted = false;
// How many "bars" to increase/decrease the volume on each button press
int volSteps = 2;
// Holds last button to have been pressed
uint64_t lastValue = 0;
// Tells whether the user is holding a button on the remote control
bool isHeld = false;
// Timestamp of last decode success notification
unsigned long last = 0;

// Pin on which TSOP3848 is connected
const uint16_t kRecvPin = 14;
const uint32_t kBaudRate = 115200;
const uint16_t kCaptureBufferSize = 128;
const uint8_t kTimeout = 50;
const uint16_t kMinUnknownSize = 12;
const uint8_t kTolerancePercentage = kTolerance;  // kTolerance is normally 25%
#define LEGACY_TIMING_INFO false
IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
decode_results results;  // Somewhere to store the results

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  randomSeed(micros());

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

unsigned long IRAM_ATTR millis2()
{
    return (unsigned long) (esp_timer_get_time() / 1000ULL);
}

void setup() {
#if defined(ESP8266)
  Serial.begin(kBaudRate, SERIAL_8N1, SERIAL_TX_ONLY);
#elif ARDUINO_USB_CDC_ON_BOOT
  Serial.begin(kBaudRate);
#else  // ESP8266
  Serial.begin(kBaudRate, SERIAL_8N1);
#endif  // ESP8266
  while (!Serial) delay(50);
  assert(irutils::lowLevelSanityCheck() == 0);
  setup_wifi();
  Serial.printf("\n" D_STR_IRRECVDUMP_STARTUP "\n", kRecvPin);
#if DECODE_HASH
  irrecv.setUnknownThreshold(kMinUnknownSize);
#endif
  irrecv.setTolerance(kTolerancePercentage);
  irrecv.enableIRIn();
}

void actOnValue(uint64_t value) {
  /*Serial.println(String("Client status: ") + client.status());
  while (client.available()) {
    char c = client.read();
    Serial.print(c);
  }
  if (!client.connected()) {
    client.setTimeout(1);
    Serial.println("Connecting to client...");
    client.connect(ADDR, PORT);
  }*/
  if (value == 0x40A2) {
    if (first) first = false;
    else volume += volSteps; 
    if (volume > 63) volume = 63;
    client.setTimeout(1);
    client.connect(ADDR, PORT);
    client.println("VOLUME 0");
    client.println(String("HEADPHONEVOLUME ") + volume);
    client.stop();
    Serial.println("volume up");
  } else if (value == 0x42A2) {
    if ((volSteps == 2 || volSteps == 4) && volume == 63) volume = 64;
    if (first) first = false;
    else volume -= volSteps; 
    if (volume < 0) volume = 0;
    client.setTimeout(1);
    client.connect(ADDR, PORT);
    client.println("VOLUME 0");
    client.println(String("HEADPHONEVOLUME ") + volume);
    client.stop();
    Serial.println("volume down");
  } else if (value == 0x43A2) {
    muted = !muted;
    client.setTimeout(1);
    client.connect(ADDR, PORT);
    client.println("VOLUME 0");
    if (muted) client.println("HEADPHONEVOLUME 0");
    else client.println(String("HEADPHONEVOLUME ") + volume);
    client.stop();
    Serial.println("mute");
  }
}

void loop() {
  if (irrecv.decode(&results)) {
    unsigned long now = millis2();
    if (now - last > 300) isHeld = false;
    if (isHeld && results.decode_type == UNKNOWN) {
      actOnValue(lastValue);
    }
    if (results.decode_type == SHARP) {
      actOnValue(results.value);
      lastValue = results.value;
      isHeld = true;
    }
    last = now;

    // Code to print info about the received bytes:
    /*
    // Display a crude timestamp.
    uint32_t now = millis();
    Serial.printf(D_STR_TIMESTAMP " : %06u.%03u\n", now / 1000, now % 1000);
    // Check if we got an IR message that was to big for our capture buffer.
    if (results.overflow)
      Serial.printf(D_WARN_BUFFERFULL "\n", kCaptureBufferSize);
    // Display the library version the message was captured with.
    Serial.println(D_STR_LIBRARY "   : v" _IRREMOTEESP8266_VERSION_STR "\n");
    // Display the tolerance percentage if it has been change from the default.
    if (kTolerancePercentage != kTolerance)
      Serial.printf(D_STR_TOLERANCE " : %d%%\n", kTolerancePercentage);
    // Display the basic output of what we found.
    Serial.print(resultToHumanReadableBasic(&results));
    // Display any extra A/C info if we have it.
    String description = IRAcUtils::resultAcToString(&results);
    if (description.length()) Serial.println(D_STR_MESGDESC ": " + description);
    yield();  // Feed the WDT as the text output can take a while to print.
#if LEGACY_TIMING_INFO
    // Output legacy RAW timing info of the result.
    Serial.println(resultToTimingInfo(&results));
    yield();  // Feed the WDT (again)
#endif  // LEGACY_TIMING_INFO
    // Output the results as source code
    Serial.println(resultToSourceCode(&results));
    Serial.println();    // Blank line between entries
    yield();             // Feed the WDT (again)
    */
  }
}

Smart Air Conditioning

The goal for this task was to be able to smart control the AC unit that I have in my home, bypassing the included remote control. The end goal was to have them controlled from anywhere around the globe.

About

The AC units are Bosch Climate 3000i . These are actually manufactured by Midea and sold under the Bosch brand in Romania . The units feature an OEM dongle option, but the functionality is limited and it comes with a steep price .

The logic board of the AC is hidden away by a plastic tray underneath the front lid. A standard Phillips screw and a set of plastic clips hold the tray and have to be carefully removed. The logic board features a female USB-A receptacle. Despite this, the electrical wiring is not for USB, but for UART . Communication with the AC unit can happen over this port using the Midea protocol .

Hardware

So, for this task, I have decided to use an ESP32 board which is wired for communication to the UART port on the AC unit logic board. I have determined that both boards use the same logic voltage levels, so there is no need for level shifters when wiring the RX/TX pins. Since some of the commands of the protocol do not work over the UART connection, I have also connected a standard 940nm infrared LED to the board, through which the remote control's commands are replicated in order to send the unsupported commands. I decided for this hybrid solution instead of going with the IR alone since the UART communication also offers feedback about the current status of the AC unit (current room temperature and other sensor data, for example).

Pinout

  • USB red wire (USB 5V) > ESP32 5V pin
  • USB green wire (USB D+) > ESP32 GPIO1 (TX) pin
  • USB white wire (USB D-) > ESP32 GPIO3 (RX) pin
  • USB black wire (USB GND) > ESP32 GND pin
  • IR LED long pin > ESP32 GPIO19 pin
  • IR LED short pin > ESP32 GND pin

Software

Since a standalone dashboard would introduce too much friction, I have decided to have this board connected to my WiFi network and integrate it with my Home Assistant setup as an additional “service”. This is best achieved by writing an ESPHome description file and having the framework glue modules together in order to produce the desired functionality. Getting a usable prototype is accelerated compared to the traditional approach of writing boiler code that links various libraries together at a high level. ESPHome is a mature ecosystem supported by Nabu Casa, the same team that delivers the world renowned Home Assistant ecosystem.

Remote Access

Home Assistant offers a HomeKit bridge service that integrates it within the Apple Home. Apple Home can be managed remotely if it contains a home hub device, such as an Apple TV or a HomePod speaker. An alternative outside the Apple ecosystem is an yearly subscription to Nabu Casa which allows access to your Home Assistant instance via their web site (75 EUR). Or the totally DIY way is to enable port forwarding on your ISP's AP/router combo, and use a service like DuckDNS on Home Assistant, along with switching it to using HTTPS using a free Let's Encrypt certificate instead and access your instance over the public web directly.

Gotchas

When crafting the male receptacle that connects with the female one on the AC's logic board, usually by sacrificing some cheap USB cable laying around, pay extra attention and I recommend to verify the pinout of the connector against the wires: cheap Chinese cables often use different colors for the wires or use the traditional colors (red/green/white/black), but connect them in a random order (I have seen 5V via the black wire and GND via the red wire, for example). Use a multimeter and check each wire against the connector - failure to do so may lead to damaged equipment, as things aren't usually protected against reverse connection, for example. You have been warned!

Code

This command can be used to compile based on the YAML description and flash a connected ESP32 board:

esphome.exe run --device COM1 config.yaml
  • “uart” section describes the pins that the module uses for UART communication
  • “remote_transmitter” section describes which pins the IR LED uses
  • “climate” section configures a Midea control module. This automatically finds and uses the “uart” and “remote_transmitter” modules to achieve its functionality.

Below is the full listing for the config.yaml file:

esphome:
  name: ac-esp
  friendly_name: AC_ESP

esp32:
  board: esp32dev
  framework:
    type: arduino

logger:
  baud_rate: 0

api:
  encryption:
    key: "fjkhgusyfhygskfhgbvfxkhvbjkdhgxukhxkghxkxrg="

ota:
  password: "d76v87687db8ebmdbfjdjb8d789bdbd7"

wifi:
  ssid: REPLACE_WITH_YOUR_SSID
  password: REPLACE_WITH_YOUR_PASSWORD

  ap:
    ssid: "Fallback Hotspot"
    password: "473958869456"

uart:
  tx_pin: 1
  rx_pin: 3
  baud_rate: 9600

remote_transmitter:
  pin: GPIO19                       # For iot-uni-stick.
  carrier_duty_percent: 50%         # 50% for IR LED, 100% for direct 
                                    # connect to TSOP IR receiver output.

climate:
  - platform: midea
    name: AC_ESP               # Use a unique name.
    period: 1s                  # Optional
    timeout: 2s                 # Optional
    num_attempts: 3             # Optional
    autoconf: true              # Autoconfigure most options.
    beeper: false               # Beep on commands.
    visual:                     # Optional.
      min_temperature: 17 °C    # min: 17
      max_temperature: 30 °C    # max: 30
      temperature_step: 1 °C  # min: 0.5
    supported_modes:            # Optional. 
      - FAN_ONLY
      - HEAT_COOL
      - COOL
      - HEAT
      - DRY
    custom_fan_modes:           # Optional
      - SILENT
      - TURBO
    supported_presets:          # Optional. 
      - ECO
      - BOOST
      - SLEEP
    custom_presets:             # Optional.
      - FREEZE_PROTECTION
    supported_swing_modes:      # Optional
      - VERTICAL
      - HORIZONTAL
      - BOTH
    outdoor_temperature:        # Optional. 
      name: Temp

switch:
  - platform: template
    name: Beeper
    icon: mdi:volume-source
    restore_mode: 'RESTORE_DEFAULT_OFF'
    optimistic: true
    turn_on_action:
      midea_ac.beeper_on:
    turn_off_action:
      midea_ac.beeper_off:

binary_sensor:
  - platform: status
    name: Connection Status
    id: climate_AC_ESP_connection_status

text_sensor:
  - platform: template
    name: Uptime
    id: uptime_human
    icon: mdi:clock-start

  - platform: version
    name: ESPHome Version
    id: climate_AC_ESP_esphome_version

  - platform: wifi_info
    ip_address:
      name: IP
      id: climate_AC_ESP_ip_address
      icon: mdi:ip-network

sensor:
  - platform: uptime
    name: Uptime Sensor
    id: uptime_sensor
    update_interval: 60s
    on_raw_value:
      then:
        - text_sensor.template.publish:
            id: uptime_human
            state: !lambda |-
              int seconds = round(id(uptime_sensor).raw_state);
              int days = seconds / (24 * 3600);
              seconds = seconds % (24 * 3600);
              int hours = seconds / 3600;
              seconds = seconds % 3600;
              int minutes = seconds /  60;
              seconds = seconds % 60;
              return (
                (days ? to_string(days) + "d " : "") +
                (hours ? to_string(hours) + "h " : "") +
                (minutes ? to_string(minutes) + "m " : "") +
                (to_string(seconds) + "s")
              ).c_str();
  - platform: wifi_signal
    name: WiFi Signal
    id: climate_AC_ESP_wifi_signal
    update_interval: 60s

button:
  - platform: restart
    name: Reboot
    id: climate_AC_ESP_restart
    icon: "mdi:restart"
  - platform: shutdown
    name: Shutdown
    id: climate_AC_ESP_shutdown
  - platform: safe_mode
    name: Reboot (Safe Mode)
    id: climate_AC_ESP_safe_mode
  - platform: template
    name: Display Toggle
    id: climate_AC_ESP_display_toggle
    icon: mdi:theme-light-dark
    on_press:
      midea_ac.display_toggle:
  - platform: template
    name: Swing Step
    icon: mdi:tailwind
    on_press:
      midea_ac.swing_step:

Conclusion

Overall, a useful implementation for my home automation goals.

References

iothings/proiecte/2023sric/esp32_home_automation.txt · Last modified: 2024/05/30 01:55 by valentin.radu2005
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0