CC Blog Projects Research & Design Hub

Home Controller Coordinates a Ductless Heat Pump 

Written by Brian Millier

With an Existing Heating System

After installing a ductless heat pump, I wondered if I could build a controller that would operate the new unit only when it was more cost-effective than my existing Electrical Thermal Storage units, which operated on time-of-day metering.

  • How can I build a controller for my ductless heat pump?
  • How can I program heating to be maximally cost-effective?
  • How can I customize my home’s heating?
  • Samsung Note 10.2 tablet
  • GUI-O Anroid App
  • ESP32
  • ESP32-DevKitC board
  • DS3231 real-time clock (RTC) chip
  • Silicon Labs AM2320 sensor
  • AM2320 temperature/humidity sensors
  • TSOP38238 IR receiver module

I recently installed a ductless heat pump at my home. To be honest, the energy savings during the heating season were a consideration, but the hotter summers we have been experiencing were getting hard to tolerate without air conditioning. One reason that I held off for so long was that the heating system installed in my 30-year-old home is what is called an Electrical Thermal Storage (ETS) unit. In my house, this consists of two units, each about four feet long, two feet high, and one foot deep. Each unit is filled with high-temperature (refractory) bricks with resistance heaters positioned between the different layers of brick.

These units are well-insulated, and the high heat attained by the bricks overnight is only slowly released to the room when the circulating fan is running. As part of this heating system, our local power company installed a time-of-day power meter and offers an electricity rate of 57% of the standard rate from 11 PM to 7 AM on weekdays and 24 hours a day on weekends and holidays. The ETS units are programmed to only draw power during these off-peak times. Since the power company has extra capacity at these off-peak periods, and also wants to avoid power being drawn at peak times, this is a win-win situation for both the utility and the customer. In my two-story, 1,800ft2 home, I have two units, each of which draws 6,000 watts and can store up to 27kWh of energy (equal to 92,174BTU). Basically, enough energy is stored overnight to heat the house for the following day.

Modern ductless heat pumps are popular today since they can readily be retrofitted to homes lacking the ductwork that a furnace would require. They are much more efficient at heating than electrical resistance heating such as the ETS units, which are inherently 100% efficient. In addition, they provide air conditioning in the summer—something that I did not even take into consideration 30 years ago when I built the house.

Since there were provincial and federal grants available if one installed a heat pump large enough to heat the whole house (for all but the coldest days in the winter), I decided to go that route. A heat pump is significantly more cost-efficient than an oil or gas furnace (especially considering recent fossil fuel prices). But my engineering background got me wondering if there was an optimal choice of when to use the heat pump and when to use the ETS units. Without doing any detailed calculations, I knew the following:

  • The ETS units are 100% efficient and draw electricity at 57% of the standard rate—making them 175% efficient relative to resistive heating at standard rates.
  • Since heat pumps don’t generate heat, but merely transfer it, they can have heating efficiencies of 100%-400% (depending upon the outdoor temperature).

In Figure 1, I have plotted the heating BTUs/kW for the Fujitsu heat pump that I installed. For comparison’s sake, resistance heating produces 3,415BTUs/kW. Let’s ignore, for now, that in my situation, the electricity rate changes over a 24-hour period.

Figure 1
This graph shows the heating BTU/kW values for my Fujitsu heat pump versus temperature.
Figure 1
This graph shows the heating BTU/kW values for my Fujitsu heat pump versus temperature.
These are the time-of-day rates currently offered by our power utility. While rates will vary by location, I expect the same idea applies for any utility.
These are the time-of-day rates currently offered by our power utility. While rates will vary by location, I expect the same idea applies for any utility.
This shows the cost to produce 10,000BTUs of heat from my Fujitsu heat pump. The blue trace is for an outside temperature of -15°C, the pink trace is for 0°C, and the yellow trace is for +10°C. The time-of-day rates for the three winter months are factored into this graph.
This shows the cost to produce 10,000BTUs of heat from my Fujitsu heat pump. The blue trace is for an outside temperature of -15°C, the pink trace is for 0°C, and the yellow trace is for +10°C. The time-of-day rates for the three winter months are factored into this graph.
This is similar to Figure 3, but it applies to the rest of the year.
This is similar to Figure 3, but it applies to the rest of the year.

You can see that the heat pump produces about 3,300BTUs/kW at an outdoor temperature of -15°C and up to about 10,000BTUs/kW at +15°C (beyond +15°C outdoors you don’t really need much heating). Compared to resistive heating at 3,415 BTUs/kW, you can see that at an outside temperature of -15°C the heat pump is roughly equal in efficiency to resistive heating. However, at an outside temperature of +15°C, the heat pump is 292% efficient compared to resistive heating (10,000/3,415).

Let’s first compare the cost of the ETS and the heat pump over the 24 hours in a day, starting in the peak of the winter (December 1 through February 1). As a reference, see Figure 2 which is the rate schedule for time-of-day power in my location. Refer to Figure 3 for a graph of the electricity cost to produce 10,000BTUs of heat, using the heat pump. The ETS units only draw power at off-peak times (2300-700 and weekends) and the cost per 10,000BTUs is calculated to be $0.27. You can see that when the outside temperature is -15°C (blue trace) it’s more expensive to use the heat pump at any time other than the off-peak time—where it is roughly equal to the ETS unit. At 0°C (pink trace) it is much cheaper to run the heat pump at off-peak times and still less expensive at all but the peak times, which are 700-1200 and 1600-2300. At an outside temperature of 10°C (yellow trace) you may as well run the heat pump whenever heat is needed as the cost varies from $0.10 to $0.25—which is less than the $0.27 cost of the ETS units.

In the spring and fall, the time-of-day rates change somewhat. Figure 4 shows a graph like Figure 3 for these milder seasons, where the heat pump is more economical to run except when the outside temperature is -15°C (blue trace). Even at -15°C, they are equal in cost during off-peak hours.

From the above calculations, it was apparent that it would be worthwhile to design a controller that would keep track of the time and the day of the week, as well as monitor the outside temperature. From those conditions, it could decide when it was more economical to run the heat pump and when it was better to turn the heat pump off and let the ETS units discharge some of the “cheap” heat that they had stored up overnight (the “off-peak” period, The ETS units have an “intelligent” control system that monitors outside temperature, and they are pretty good at deciding, in advance, how much energy they need to store overnight to make it through the next day. Once a pattern of using the heat pump for all but peak electricity hours (or low outside temperatures) is established, the ETS units should intelligently adapt. I installed the heat pump in July of 2022, and at the time of writing, I had not been through a full winter and was waiting to see how it turns out.


I don’t want this article to appear as a commercial for Fujitsu ductless heat pumps. They were the units recommended by the dominant installers in the area, and their top-end models were among those that were eligible for the government grants mentioned earlier. The Fujitsu heat pump model that I bought did not come with many of the fancy gadgets that are standard on some other companies’ models, such as Bluetooth or Wi-Fi capability (allowing you to adjust them while you were driving home from work, and so on) [1]. I wasn’t really interested in the latter feature, as I’m retired and generally at home.

The standard Fujitsu heat pump uses an IR remote control to operate it. If you have followed my column in the past, you may recall seeing several IR remote control project articles that I wrote [2][3][4]. (These included my two-part article “Build an iPad-Based IR Remote” in Circuit Cellar 310 and 311, May and June 2016; and “Build a Touchscreen IR Remote: Using an ESP32 MCU” in Circuit Cellar 380, March 2022.) Therefore, I figured it would be easy enough to build a controller that could turn the heat pump on/off and possibly adjust the set-point temperature using an IR signal. This would be entirely non-invasive—that is, you could still use the Fujitsu remote to override the controller’s commands, if necessary.

Also in an earlier Circuit Cellar series (“Using ESP-NOW Protocol,” Parts 1 and 2 in Circuit Cellar 368 and 370, March and May 2021), I described a home controller I designed that controlled an air exchanger in my house, and an irrigation pump which I use to draw water from the lake that I live on. It also monitors several Wi-Fi-based water leak detectors that I built and installed in the basement [5][6].

I decided to retire this older unit and replace it with one that controls the heat pump, the air exchanger, and the irrigation pump. I didn’t include the leak detectors in this new design. Originally, the water leak detectors could send me an email and SMS message if any water was detected, which I thought was useful while I was still working. However, the cloud services that I used to implement this feature have changed over time, and that capability no longer works.


A controller such as this requires a comprehensive “front panel” since there needs to be a 24-hour matrix in which you can select what you want to occur in each one-hour time slot. Also, I decided to incorporate a “learning” IR remote—that is, the controller would “learn” each of the IR codes that it needs to send. In this case, there were 11 commands to learn, so the front panel must display all the command buttons. The user selects each button before the IR remote is aimed at the controller and the chosen remote-control button is pressed.

For this part of the controller, I decided to use a “virtual” front panel. Basically, this controller would have only needed a touchscreen display when the user is actually programming in the various actions for the 24-hour time slots (for both weekdays and weekends). This only happens occasionally. The IR code learning operation will only be used once, during initial configuration.

Therefore, I decided to incorporate the GUI-O Android application running on my Samsung Note 10.2 tablet. I described the GUI-O Android app in my article “GUI-O: A ‘Virtual’ Front Panel: For ESP32 Projects” (Circuit Cellar 389, December 2022) [7]. This application communicates with the ESP32 microcontroller (MCU) that runs the controller. The GUI-O app is basically an Android application that can parse incoming commands and convert them into all of the widgets needed for a comprehensive graphic user interface (GUI). The GUI-O app can communicate with the external MCU using legacy Bluetooth 4, Bluetooth LE or even Wi-Fi. For this project, I used legacy Bluetooth 4, as it was the easiest and fastest protocol for my purposes.

In conjunction with the GUI-O Android app, I wrote an Arduino sketch, which runs on an ESP32. This sketch contains two functions that handle the following:

  • Rendering the IR remote-control learning screen and responding to touches.
  • Rendering the screen for the allocation of commands for the heat pump, air-exchanger, and irrigation pump, for each of the 24 one-hour time slots (both weekdays and weekends), and responding to touches.

Each routine contains the various commands needed to display the necessary widgets on the Android tablet’s screen. Each routine also contains code to parse out the status reports coming back from the tablet when various buttons, sliders, and the time slot matrix are touched. For each widget touched, this parsing routine will extract whatever value is associated with that widget, and either set an ESP32 variable or execute a routine, as necessary. Listing 1 shows the code needed to generate the IR learning remote screen as displayed in Figure 5. The sendMsg function sends the string parameter specified out via the Bluetooth port. GUI-O uses the Bluetooth Serial Port Profile (SPP), which basically emulates a UART or serial port connection. Listing 2 is a section of the routine that parses incoming Bluetooth messages for the various GUI-O strings that get returned when buttons and so forth are pressed. Note that on the IR learning screen there are nine buttons for the different heating temperatures. On Fujitsu’s IR remote control, there is only an UP/DOWN button for adjusting the set-point temperature. The remote control doesn’t issue UP/DOWN IR codes. Instead, it issues a separate code for ON, OFF, and each of the setpoint temperatures. All these codes are different depending on whether the IR remote is set for heating or cooling mode. Therefore, when you are learning the IR codes, you must ensure that the IR remote is in heating mode before proceeding with the learning process. In the summer, the heat pump is used for air conditioning. In this case, it was more useful to just turn the unit on and off manually, as needed. I didn’t build into this controller the capability to control the heat pump while in cooling mode.

This is the section of the controller program that loads the Android app’s screen with the widgets for the IR Learning portion of the user interface.
// clear screen and set background
        sendMsg(“@guis BGC:#FFFFFF\r\n”);
        // initialize the IR learning screen GUI
        sendMsg(“|BT UID:bt0 X:50 Y:5 W:30 FSZ:5 TXT:\”Power ON\”\r\n”);
        sendMsg(“|BT UID:bt1 X:50 Y:13 W:30 FSZ:5 TXT:\”Power OFF\”\r\n”);
        sendMsg(“|BT UID:bt2 X:50 Y:21 FSZ:5 TXT:\”28C\”\r\n”);
        sendMsg(“|BT UID:bt3 X:50 Y:29 FSZ:5 TXT:\”27C\”\r\n”);
        sendMsg(“|BT UID:bt4 X:50 Y:37 FSZ:5 TXT:\”26C\”\r\n”);
        sendMsg(“|BT UID:bt5 X:50 Y:45 FSZ:5 TXT:\”25C\”\r\n”);
        sendMsg(“|BT UID:bt6 X:50 Y:53 FSZ:5 TXT:\”24C\”\r\n”);
        sendMsg(“|BT UID:bt7 X:50 Y:61 FSZ:5 TXT:\”23C\”\r\n”);
        sendMsg(“|BT UID:bt8 X:50 Y:69 FSZ:5 TXT:\”22C\”\r\n”);
        sendMsg(“|BT UID:bt9 X:50 Y:77 FSZ:5 TXT:\”21C\”\r\n”);
        sendMsg(“|BT UID:bt10 X:50 Y:85 FSZ:5 TXT:\”20C\”\r\n”);
        sendMsg(“|LB UID:lb11 X:80 Y:50 TXT:\”IR received\”\r\n”);
        sendMsg(“|SI UID:si12 X:80 Y:55\r\n”);
        sendMsg(“|LB UID:lb13 X:50 Y:95 FSZ:5 TXT:\”Fujitsu Remote Learning\”\r\n”);
        sendMsg(“|BT UID:bt11 X:20 Y:21 FGC:#EB0A46 FSZ:5 TXT:\”28C\”\r\n”);
        sendMsg(“|BT UID:bt12 X:20 Y:29 FGC:#F00C04 FSZ:5 TXT:\”27C\”\r\n”);
        sendMsg(“|BT UID:bt13 X:20 Y:37 FGC:#F90200 FSZ:5 TXT:\”26C\”\r\n”);
        sendMsg(“|BT UID:bt14 X:20 Y:45 FGC:#FF0000 FSZ:5 TXT:\”25C\”\r\n”);
        sendMsg(“|BT UID:bt15 X:20 Y:53 FGC:#FF0000 FSZ:5 TXT:\”24C\”\r\n”);
        sendMsg(“|BT UID:bt16 X:20 Y:61 FGC:#FF0000 FSZ:5 TXT:\”23C\”\r\n”);
        sendMsg(“|BT UID:bt17 X:20 Y:69 FGC:#FF0000 FSZ:5 TXT:\”22C\”\r\n”);
        sendMsg(“|BT UID:bt18 X:20 Y:77 FGC:#FF0000 FSZ:5 TXT:\”21C\”\r\n”);
        sendMsg(“|BT UID:bt19 X:20 Y:85 FGC:#FF0000 FSZ:5 TXT:\”20C\”\r\n”);
        sendMsg(“|BT UID:bt20 X:80 Y:80 H:15 FGC:#00FFC5 TXT:\”Exit/Save\”\r\n”);
        sendMsg(“|LB UID:lb10 X:20 Y:90 FGC:#FF0000 FSZ:5 TXT:\”HEAT\”\r\n”);
        sendMsg(“|LB UID:lb12 X:50 Y:90 FSZ:5 TXT:\”COOL\”\r\n”);
This is a screen capture of the GUI-O screen for the IR learning mode.
This is a screen capture of the GUI-O screen for the IR learning mode.
This code snippet illustrates how messages from the Android GUI-O app are parsed and assigned to controller program variables.

   else if (msg.startsWith(“@bt0 Button”)) {
        Serial.println(“Power ON pressed”);
        ButtonIndex = 0;
        ButtonPressed = true;

    else if (msg.startsWith(“@bt1 Button”)) {
        Serial.println(“Power OFF pressed”);
        ButtonIndex = 1;
        ButtonPressed = true;

Figure 6 shows the time slot allocation screen for the various commands. Any time slot that has commands already assigned to it will be colored green (except that the heat pump’s “No_Action” command does not color the time slot green).

This is the screen capture of the GUI-O screen for the Time Slot command entry. Heat Pump, Air Exchanger and Irrigation Pump each have their own scrolling list to select the proper command. Each time slot must be saved (to RAM) individually, and only when the Exit button is pressed will the values be stored in EEPROM.
This is the screen capture of the GUI-O screen for the Time Slot command entry. Heat Pump, Air Exchanger and Irrigation Pump each have their own scrolling list to select the proper command. Each time slot must be saved (to RAM) individually, and only when the Exit button is pressed will the values be stored in EEPROM.

I used the ESP32-DevKitC board which contains a WROOM32 module [8]. If you’re familiar with the initial ESP32 model, you’ll know that it features both Bluetooth and Wi-Fi capabilities. I use the Bluetooth capability to communicate with my Android tablet running the GUI-O app. Both GUI-O front panel screens are only needed occasionally for setting up the time slots, and only once for the IR learning process. The rest of the time, the ESP32 runs with the Wi-Fi turned on, and it connects to my Wi-Fi access point. The ESP32 synchronizes a DS3231 real-time clock (RTC) chip periodically, using an NTP server. It also periodically accesses a weather service to get the outdoor temperature and humidity in my area. Since the ESP32 has only one RF receiver/transmitter, it cannot handle Bluetooth and Wi-Fi simultaneously. However, in this project, at boot-up, the program checks the mode switch to see whether it is in the operational mode (Wi-Fi is enabled) or one of the configuration modes (Bluetooth is enabled). Therefore, there are no scenarios in which the EPS32 is enabling the drivers for both Bluetooth and Wi-Fi.

Now that Espressif has released several new models of ESP32, it’s important to note that while they all support Wi-Fi, they don’t all support legacy Bluetooth. The ESP32-S2 doesn’t support Bluetooth at all, and the S3 and C3 only support BLE 5.0.

When I started the project, I knew that I would need a real-time clock. Since I was planning on using Bluetooth, I wasn’t initially sure if I would have Wi-Fi connectivity, so I included a DS3231 [9], which is an accurate RTC, and performed the initial setting of the DS3231 using a GUI-O function which reports the time from the Android tablet. Once I knew that I would have Wi-Fi capability, I could have eliminated the DS3231 and relied upon the ESP32’s internal RTC synced to an NTP time server. But at that point, it didn’t make much sense to unsolder the DS3231 from the board.


To properly assess how to operate the Fujitsu heat pump as well as the air exchanger, I needed to monitor the temperature and humidity, both inside and outdoors. In the past, I’ve done this using an AM2320 temperature/humidity sensor or the more accurate Si7021 from Silicon Labs. In this project, I still use the AM2320 sensor for the indoor readings [10]. However, using these sensors outdoors has its downsides, as I’ve discovered in the past. You don’t want to let them get wet, and the temperature reading you get from them will be way too high if you have to locate them somewhere that gets direct sunlight. Also, the reported humidity readings become quite inaccurate when the temperature gets lower—in other words, in the winter. As a further complication, both sensors use an I2C bus protocol. I2C doesn’t lend itself to long connecting leads, which are likely to be needed if the sensor is placed outdoors and out of the sun.

Time marches on, and today the easiest way to monitor the outside temperature and humidity is to utilize one of the many online weather services. For this project, where I only request one weather reading per hour, these services are often free.

I chose what is probably the most commonly available service, OpenWeatherMap, as it is easy to use. Initially, you must set up an account with them, but you can choose the free tier which allows for up to 60 requests per minute—much more than is needed here [11]. Once you’ve signed up, you’ll receive an email with your own, unique API key. Note: You must wait a few hours before this API key will work.

You then construct a URL which is made up of the following String, with your personal API key added at the end:,ca,&APIID=

Near the start of my program, I define the following const Strings:

const String endpoint = “,ca&APPID=”;

const String key = “224f25a91c77ebf80548f07b1aa68367”; // not my actual key!

Then I concatenate these two Strings in my code into a single String and use the ESP32 HTTPClient to GET information from that URL. Note that at the end of the endpoint String is “q=Hubley,ca”. You have to substitute your own location of course. In my case, when I see the weather on my phone, it automatically knows my location from GPS, and it displays “Hubley” as the nearest town/city from which weather information is available. I happen to live in Hubley, so the nearest weather recording site is not too far away.

To test both your API key and location, you can take this concatenated string and merely paste it into your web browser as the URL. Depending upon your browser, you may see several lines of ASCII characters, comma- and {}-delimited. Or, if your browser supports JSON parsing, you may get back a neatly organized table with many weather parameters included. Obviously, this API returns a JSON file.

The Arduino IDE has many libraries available, and a JSON library is available using the Manage Libraries tool. While I have used the JSON library in the past, for this project, I decided to just do my own parsing for the “temp:” and “humidity:” keywords using the String class indexOf and substring functions to separate out the temperature and humidity readings. One reason I did it this way was that I noticed that after adding the 13 libraries used by my program, the 1.4Mb Program Flash partition (the default ESP32 setting in the Arduino IDE) was being filled up even before I finished writing my own code. It was easy enough to switch to another partition scheme: 2Mb Program Flash and 2Mb SPIFFS (no OTA). However, I decided against including the JSON library, as I suspect it takes up lots of Program Flash. Listing 3 is the code snippet that requests the weather packet and parses it for the temperature and humidity values.

This code snippet is used to connect to the OpenWeatherMap API and retrieve the local weather. I only make use of the temperature and humidity readings.

bool getOpenWeatherMapData(void) {
    HTTPClient http;
    http.begin(endpoint + key); 
//Specify the proper URL for OpenWeatherMAp API for user’s key
    int httpCode = http.GET();  //Make the request

    if (httpCode > 0) { //Check for the returning code

        payload = http.getString();
        String temperature;
        String humidity;
        int idx = payload.indexOf(“temp”);
        temperature = payload.substring(idx + 6, idx + 12);
        float temp = temperature.toFloat();
        temp = temp - 273;
        OutdoorTemperature = temp;
        idx = payload.indexOf(“humidity”);
        humidity = payload.substring(idx + 10, idx + 12);
        float hum = humidity.toFloat();
        OutdoorHumidity =  hum;
        Serial.print(“Outdoor Temperature= “);
        Serial.print(“Outdoor humidity= “);
        return true;
    else {
        Serial.println(“Error getting OpenWeatherMap weather”);
        return false;

There are two arrays of numbers in this program which must be non-volatile (in other words,they must persist over a power-down cycle or power failure). The first array is the one which stores the raw IR codes for each of the buttons on the remote. The Fujitsu IR codes are much more complex than those I have observed in the past when doing other IR projects. Figure 7 is a ‘scope capture of the Fujitsu POWER OFF code. The high and low times for each pulse must be measured and stored. To be safe, I decided to allow for a maximum of 250 pulses (the eventCount constant in the program). In practice, I found that virtually all the IR codes contained 128 pulses with a few (POWER OFF) containing only 56. Nevertheless, the size of this array is [2 bytes (for space duration) + 2 bytes (for pulse duration)] x 250 pulses x 11 buttons. I allocated 22,000 bytes in case I decided to add cooling functions later.

This is a 'scope capture of the Fujitsu “POWER OFF” IR code. This code is one of the few having only 56 pulses—most others contain 128.
This is a ‘scope capture of the Fujitsu “POWER OFF” IR code. This code is one of the few having only 56 pulses—most others contain 128.

To store this 22,000-byte array, I decided to use the ESP32’s SPIFFS class. This is basically a file system that uses a partition in Flash to store the data. This class includes wear leveling since the ESP32 program Flash supports only 100,000 write cycles. In this case, I only write this file a limited number of times, since it is storing IR codes that don’t change. Note that I didn’t claim that I only wrote this file once: when you choose a different partition scheme (which I did, as described earlier), the SPIFFS file system gets clobbered, and the codes must be learned/stored again.

The other array that needs to be stored is the time slot array. There are 24 time slots allocated for weekdays and 24 more for weekends. I’m using 11 different IR codes for the Fujitsu heat pump, which I store in a 5-bit field. There are three different actions associated with the air exchanger (2-bits) and a simple on/off command for the irrigation pump (1-bit). Therefore, a single byte can handle all the actions that need to be stored in any given time slot. While I could have saved this 48-byte array to the SPIFFS file system, I decided instead to use the EEPROM class which can handle these 48 bytes as a structure, using just a single write (or read) command.

I get my AM2320 temperature/humidity sensors from Adafruit mainly because they provide a good Arduino library to use them. Adafruit is quite serious about supplying Arduino libraries for most (if not all) of the sensors they sell. Also, they provide Circuit-Python libraries for their sensors as well. To organize this better, they’ve created an abstraction layer file which is needed by all the sensor libraries that they’ve written: Adafruit_Sensor.h. This file, as well as the Adafruit_AM2320.h, must be present in the #include section at the head of the program. These two libraries can be downloaded directly from within the Arduino IDE using the Manage Libraries tool.


The two display screens for configuring the time slots and the IR learning process are done using an Android tablet/phone running the GUI-O software. However, the rest of the time, the unit is in operational mode. Wi-Fi is required in this mode, so the Android tablet/GUI-O interface (via Bluetooth) cannot be used.

I wanted to be able to show a fair amount of real-time status information. Figure 8 shows the information that is displayed on the 20×4 character LCD display. Line 1 is the last command sent to the Fujitsu heat pump, and line 2 shows the last command sent to the air exchanger. Line 3 alternates between the inside and outside temperature/humidity every 30 seconds. Line 4 is the current date and time (in 24-hour format). I didn’t have space to display the irrigation pump status, but since it is only on/off, I have an LED on the front panel for this.

This is a photo of the 20x4 character LED display used for displaying current conditions. The third line switches between inside and outside values every 30 seconds.
This is a photo of the 20×4 character LED display used for displaying current conditions. The third line switches between inside and outside values every 30 seconds.

The ESP32-DevKitC has 29 GPIO lines pinned out, but many of them are pre-allocated to various internal functions (internal QSPI flash, boot-mode control lines needed during reset, and so on). Beyond these, I used five GPIOs for switches, two for the I2C bus, two for relays, two for IR receive/send, one for LED, and two for a rotary encoder. That did not leave enough free GPIOs for the standard character LCD panel, which needs at least six GPIOs. Currently, 20×4 character displays cost about $12 USD and they often come with an I2C interface board thrown in for free. I used this I2C interface for the LCD display; it eliminated the need for any dedicated GPIO pins for the LCD itself. The I2C interface board uses the popular NXP PCF8574 I2C-to-GPIO bridge chip. There is a “LiquidCrystal_PCF8574” Arduino library for these I2C interface displays.

The last display feature that I wanted to include was a way to scroll through all 24-hour time slots (weekdays and weekends) and check out the current heat pump, air exchanger, and irrigation pump commands for a given time slot. To facilitate that, I added a rotary encoder. In the slot display mode (selected at boot-up by monitoring the mode switches), the LCD display shows all three command entries for a given time slot, and the rotary encoder scrolls through all 24 one-hour weekday slots, followed by the 24 weekend slots.

Not all Arduino rotary encoder libraries work properly with the ESP32. I used the library titled “ESP32encoder” by Kevin Harrington, which can be obtained from within the Arduino IDE using the Manage Libraries tool.


The ESP32 contains a dedicated remote control transceiver (RMT) internal hardware block designed to capture and/or transmit IR codes. In my earlier Circuit Cellar IR articles, I figured out how to use this RMT block to send IR codes. To do this, I had to include the “esp32_rmt.h” file and make some modifications to Espressif’s rmt.c and rmt.h files. Since then, I’ve updated my Arduino IDE to the latest version ESP32 core v2.03 (which supports the newer ESP32-S2, S3, and C3 devices that interest me). Unfortunately, changes have been made to the core routines, and the IR routines that I had earlier implemented using the RMT block no longer work. Time marches on!

Since I didn’t want to reinvent that wheel, for this project, I wrote IR routines that just used the ESP32’s internal microsecond timer to measure the IR pulse and space times. These routines were similar to code I had previously written for an AVR MCU.


Advertise Here

All IR remote controls use IR pulses that are modulated at some fixed carrier frequency. This modulation is used to allow the IR receiver to incorporate a bandpass filter set to this carrier frequency. Using such modulation makes the whole IR remote control scheme much less susceptible to interference from stray light.

The most common IR carrier frequency is 38kHz, but this can vary from 25kHz to 56kHz depending on the particular IR remote control device. I saw on the web that the Fujitsu IR remote used 38kHz, but I wanted to be sure. Most IR receiver modules contain a bandpass filter at a specified carrier frequency and a demodulator. This type of receiver module can’t be used to determine the carrier frequency of an IR remote control. I have another project which uses the Vishay TSMP1138 IR receiver [12]. This is specifically designed for learning remotes and IR “blasters” because it contains neither a bandpass filter nor a demodulator. Aiming the Fujitsu remote at this receiver confirmed that it used a 38kHz carrier frequency. Therefore, for this project I used a TSOP38238 IR receiver module, which is designed for 38kHz.

To send out the IR codes, it’s necessary to modulate the pulse train with this 38kHz carrier. If you use the ESP32’s RMT block, this modulation is easy to achieve. You program the RMT block to use modulation and set a register to define the carrier frequency. That’s all that is needed—the signal coming from the ESP32’s GPIO pin is a modulated pulse train that just needs to be fed to a driver transistor and an IR LED.

Since I was not using the RMT block, I had to do a bit more work. I used the ESP32 ledc block to generate a 38kHz square wave using the following code snippet:


// This pin generates the 38 KHz carrier used for the IR signal

ledcSetup(0,38000,8); // set ledc PWM to 38 KHZ 8 bit resolution

ledcWrite(0,128); // 50% duty cycle

How do I do the actual modulation of the IR pulses? Referring to Figure 9, you can see that T1 is the 2N3904 NPN transistor driving the IR LED. Its base is pulled high by R6. Diode D1 connects to GPIO5, which is the pin that sends out the 38kHz square wave signal. Diode D2 connects to GPIO19, the pin that sends the IR pulse train. Diodes D1 and D2 act as an AND gate—unless both the carrier and the IR pulse are high, the drive to T1 is low, so the IR LED is not lit. That is how this circuit performs the modulation. Resistor R5, 22Ω is used because, even with both carrier and pulse signals low, the diodes will still drop about 0.5V. This is close to what is needed to turn on T1, but any tendency for it to do so would result in a voltage drop across R5. This negative feedback will minimize the current through T1, during the LED off period.

I admit that I’d rather spend time building this extra little circuit than delving into whatever changed in the ESP32 core v2.03 that broke my older RMT-based IR routines.

This is a complete schematic of the unit. The K2 relay is the only component not in the enclosure; it resides in an electrical box near my Power Entrance panel.
This is a complete schematic of the unit. The K2 relay is the only component not in the enclosure; it resides in an electrical box near my Power Entrance panel.
This is the rather unique Recom 5V power supply that I used. It is contained in a standard IEC-C14 receptacle.
This is the rather unique Recom 5V power supply that I used. It is contained in a standard IEC-C14 receptacle.

Figure 9 is the schematic of the project. The only parts that I haven’t already covered in the text are:

  • Power for the unit is 5V DC which is supplied by a neat little module: the Recom RAC05-05SK/C14 [13]. The “C14” might give it away—this 5V 1A power supply is contained within a “C14” IEC socket (Figure 10). My thanks to Mark Demarais at Recom, who sent me a few of these to try out.
  • The rotary encoder is an inexpensive Bourns PEC11-4215F-S24. These mechanical encoders don’t produce the cleanest quadrature signals, so the 0.1µF capacitors across A and B terminals to ground are essential.

There are four modes of operation used in this unit. Of those, three are configuration modes that are done only infrequently:

  • IR learning mode: This is only done once to capture the IR codes of the Fujitsu remote control. It uses the Android phone/tablet running GUI-O.
  • Time slot entry mode: This screen allows you to enter the actions that you want done for each 24-hour time slot, weekdays and weekends. It also uses the Android phone/tablet running GUI-O.
  • Time slot display: This mode allows you to scroll through the various time slots to see what commands have been programmed. The front panel 20×4 character LCD display and the rotary encoder are used for this mode
  • Operational mode: This is the mode that is running 99.99% of the time. It keeps track of time/date, indoor/outdoor temperature/humidity, and it controls the heat pump, air exchanger, and irrigation pump.

On the schematic, I show three discrete push-button switches for the first three modes. In reality, I used a four-position rotary switch instead, and the fourth (unconnected pole) corresponds to the operational mode. To enter any of the four modes, power the unit down, switch to the desired mode, and power the unit back up again. Since the first three modes are used so infrequently, this didn’t seem to me to be much of an inconvenience. I also included SEND ON and SEND OFF buttons. These are used to ensure that the IR codes are correctly received by the heat pump.

Figure 11 is the finished unit. The curious little “finger” protruding from the right side contains the IR LED and is adjusted to point to the heat pump “head” unit. Figure 12 shows the interior circuitry.

This is a front view of the unit. The red and green buttons can be pressed at any time to turn the Fujitsu heat pump on/off (used to ensure the IR LED is pointing at the heat pump's inside “head” unit properly). The blue and orange item protruding from the right side is the flexible “finger” containing the IR LED.
This is a front view of the unit. The red and green buttons can be pressed at any time to turn the Fujitsu heat pump on/off (used to ensure the IR LED is pointing at the heat pump’s inside “head” unit properly). The blue and orange item protruding from the right side is the flexible “finger” containing the IR LED.
This photo shows the complete circuit (less the IEC-C14 power module which connects to the wire exiting the top of the photo).
This photo shows the complete circuit (less the IEC-C14 power module which connects to the wire exiting the top of the photo).

I have been in contact with the developer of this Android app for testing, debugging, and some feature suggestions. I find it to be a stable app and it is, in my opinion, the easiest way to develop an Android phone- or tablet-based GUI for your MCU project. It communicates wirelessly, so you need Bluetooth, BLE, or Wi-Fi connectivity, but if you are using ESP32 MCUs, that’s not an issue.

As I am writing this article, GUI-O is giving away free licenses for this Android app. But I expect that this promotion will be over by the time you read this, and the regular price of $10 USD will apply.


I wrote the article right after building the project in late fall. The actual heat pump controller part of the project has been tested—in other words, the IR remote signals are being received fine by the heat pump “head” unit. I ran into an unexpected complication with my model Fujitsu heat pump. I have two head units connected to one outdoor compressor unit. We never intended the head unit in our bedroom to be used for heating in the winter for several reasons: very little “extra” heating is needed in the bedroom while sleeping, and I expected the fan noise would keep us awake at night. However, when the head unit on the first floor is producing heat, the head unit in the bedroom makes an annoying gurgling noise (refrigerant flow), even when it is powered off. This makes it hard to sleep. The installer has not been able to fix this and claims it’s normal, even though I’ve disproved this by observing a neighbor’s similar Fujitsu setup which doesn’t exhibit this problem.

Therefore, I programmed my controller to turn the first-floor unit off all night long, allowing us to sleep. So, we’re not getting the energy savings we expected by using the off-peak electricity overnight. Luckily, our thermal storage units are still working overnight. The air exchanger and irrigation pump features are working as designed. 

[1] Fujitsu Heat pump specifications:[2] Millier, Brian: Build an iPad-Based IR Remote (Part 1): The Original Design and IR Code Protocols. Circuit Cellar, Issue 310, May 2016, p. 12-19[3] Millier, Brian: Build an iPad-Based IR Remote (Part 2): System Circuitry and Programming. Circuit Cellar, Issue 311, May 2016, p. 26-33.[4] Millier, Brian: Build a Touchscreen IR Remote: Using an ESP32 MCU. Circuit Cellar, Issue 380, March 2022, p. 59-69.[5] Millier, Brian: Using ESP-NOW Protocol (Part 1): A Low-Power Wi-Fi Alternative. Circuit Cellar 368, March 2021, p. 56-63.[6] Millier, Brian: Using ESP-NOW Protocol Part 2: Hardware Description. Circuit Cellar 370, May 2021, p. 52-59.[7] Millier, Brian: GUI-O: A “Virtual” Front Panel: For ESP32 Projects. Circuit Cellar 389, December 2022, p. 28-38.[8] ESP32-DevKitC:[9] DS3231 RTC:[10] AM2320 Temperature/Humidity sensor:[11] OpenWeatherMap API Service:[12] Vishay IR Receiver:[13] Recom RAC05-05SK/C14:

Analog Devices |
Espressif Systems |
RECOM Power |
Vishay |

Code and Supporting Files


Advertise Here


Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.

Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
+ posts

Brian Millier runs Computer Interface Consultants. He was an instrumentation engineer in the Department of Chemistry at Dalhousie University (Halifax, NS, Canada) for 29 years.

Supporting Companies

Upcoming Events

Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

Home Controller Coordinates a Ductless Heat Pump 

by Brian Millier time to read: 27 min