Controlling IoT devices with Alexa and Apple Home Together

This project originally started as a way for me to control my non-smart TV and AC with Alexa which I completed and wrote about in my previous post. But my strive for perfection turned into a 3-4 month project to add the ability to use Apple’s Home app to control all the smart devices in our home. Resulting in everyone in my home having seamless and easy control of all the smart devices in our home either with Alexa or from the control center of their iPhones.

In my home, we have Amazon Alexa for our smart lights and plugs, but we are also tied to the Apple ecosystem. Everyone uses iPhones, iPad, Mac and we have the Apple TV. Alexa smart devices are not supported by Apple Home.

The simple solution would be to just get Apple’s Homepod (Alexa Echo alternative). However, not only would this cost $99 for the Homepod, but we would also have to change all the smart lights and plugs in our house to Apple supported ones, which costs way more than smart devices supported by Alexa.

I still wanted everything to work perfectly, without paying hundreds and I really wanted everyone to have access to smart devices with Home App.

iPhone Control Center with light control

How did I build this project?

Here is an architecture diagram of my project, which visualizes how the system works:

Home Assistant

The main component of this system which connects all the other components is Home Assistant (HASS) running on a Raspberry Pi B, which is running locally continuously. (You can also just run HASS on an AWS EC2 instance, I just had a Raspberry Pi lying around)

Home Assistant is an open-source home automation software, which allows me to connect various software through integrations.

Setting up Home App with HASS

The HASS homekit bridge integration allows me to control all selected devices in the HASS in the Home App by adding the integration to Home App as a hub. This is a pretty easy process.

Setting up smart devices with local tuya and Tuya

I realized that the smart lights and smart plugs in my home all use the Tuya smart home app. Initially, I tried using the official Tuya integration, but every time I turned off/on a light with Alexa there would be a significant delay to show the state change on the Home App. This was because the Tuya integration would have to poll the Tuya server for state changes, which had to be about every 5 minutes. Alexa on the other hand doesn’t have this problem, since it is able to query every few seconds when it has to. This was no good, I wanted everything to work instantly and seamlessly.

The solution to this was the local tuya custom integration. Setting this up came with its fair share of challenges, but it would run everything locally no need to poll Tuya servers, so it can receive instant state changes directly from the smart device.

To install the integration I had to manually place its files in the appropriate location. The challenge came when I had to get the unique local id of every smart device I wanted to set up with local tuya. This was a confusing process with not much information. I had to make a Tuya developer account and get the id from there. This video really saved me: How to video, it has easy to follow step-by-step instructions to obtain the id and the whole process was free. Once I got the id, I set up every smart light and plug with tuya local.

Since the Alexa Tuya Skill already worked well and I realized that setting up every device to work with my own smart home skill would be too much unnecessary work, I stuck with it.

Finally, this made Alexa and Home App be able to control smart lights and plug seamlessly with instant state changes. But I still needed control over my IoT devices.

Setting up ESPHome for my universal IR remote IoT device

To control my IoT device, I configured it with ESPHome.

ESPHome is software that allows me to use my ESP8266 wemos D1 mini IoT device (which before I programmed with C++ on the Arduino IDE) with HASS with the ESPHome integration. It used YAML for configuration, which is a lot for configuration on HASS. I installed the ESPHome configuration to my IoT device with the ESPHome command-line tool.

ESPHome does provide a way to configure IR signal transmission, but since I already tested the IRRemoteESP8266 library to be working perfectly before I decided to make my own custom C++ code to transmit IR signals with the library. I made the code to work with IR codes for my AC and a different one to work with different buttons on my TV. To make the code for my AC I used the default AC IR transmitter file from ESPHome as reference:

#include "esphome.h"
#include "IRremoteESP8266.h"
#include "IRsend.h"

int kIrLed = D2; // SET TO D2 for LIVING ROOM
IRsend irsend(kIrLed);

const uint8_t TEMP_MIN = 17;  // Celsius
const uint8_t TEMP_MAX = 30;  // Celsius
const float TEMP_STEP = 1.0f;
const int DELAY = 100; // ms

const int acPower=0x10AF8877; // POWER ON OFF NEC
const int acTempUp=0x10AF708F; // AC TEMP UP NEC
const int acTempDown=0x10AFB04F; // AC TEMP DOWN NEC
const int acTimer=0x10AF609F; // AC TIMER BUTTON NEC
const int acCool=0x10AF906F; // AC COOL BUTTON NEC
const int acEcon=0x10AF40BF; // AC ECON BUTTON NEC
const int acFanOnly=0x10AFE01F;

const int testfanPower=0xD81;

const bool useFahrenheit = true;
const float bootTemp = 21.1111;
static const char *const TAG = "climate";


void adjustLoop(int adjustCount, int irCode, int extra) {
  for (int i=0; i < abs(adjustCount) + extra; i++)
       irsend.sendNEC(irCode);
  delay(DELAY);
}
void adjustIfElse(int adjustCount, int irUP, int irDOWN, int extra=0) {
 if (adjustCount > 0)
      adjustLoop(adjustCount, irUP, extra);
  else if (adjustCount < 0)
      adjustLoop(adjustCount, irDOWN, extra);
}

class AC_Climate : public Component, public Climate {
 public:
  void setup() override {
    // This will be called by App.setup()
    auto restore = this->restore_state_();
    if (restore.has_value()) {
      restore->apply(this);
    } else {
      this->current_temperature = bootTemp;
      this->target_temperature = bootTemp;
    }
    irsend.begin();
  }
  void control(const ClimateCall &call) override {
    if (call.get_mode().has_value()) {
      // User requested mode change
      ClimateMode mode = *call.get_mode();
      String string_mode = climate_mode_to_string(mode);
      // Send mode to hardware
      // ...
      // irsend.sendSymphony(testfanPower);

      if (mode != this->mode) {
        if (string_mode == "OFF")
          irsend.sendNEC(acPower);
        else {
          if (climate_mode_to_string(this->mode) == "OFF")
            irsend.sendNEC(acPower);

          if (string_mode == "COOL")
            irsend.sendNEC(acCool);
          else if (string_mode == "FAN_ONLY")
            irsend.sendNEC(acFanOnly);
          else if (string_mode == "AUTO") // Same as turn on for alexa
            irsend.sendNEC(acEcon);
        }
      }


      this->mode = mode;
    }
    if (call.get_target_temperature().has_value()) {
      // User requested target temperature change
      float temp = *call.get_target_temperature();
      float currentTemp = this->current_temperature;
      String currentMode = climate_mode_to_string(this->mode);
      // Send target temp to climate
      // ...

      if (currentMode == "COOL")
        irsend.sendNEC(acCool);
      else if (currentMode == "FAN_ONLY")
        irsend.sendNEC(acFanOnly);
      else if (currentMode == "AUTO") // Same as turn on for alexa
        irsend.sendNEC(acEcon);


      if (temp != currentTemp) {
        int tempChange = int(round((temp * (9.0 / 5.0)) + 32) - round((currentTemp * (9.0 / 5.0)) + 32));
        ESP_LOGD(TAG, "F Temp Change: %d",  tempChange);

        adjustIfElse(tempChange, acTempUp, acTempDown);
      }

      this->current_temperature = temp;
      this->target_temperature = temp;
    }
    this->publish_state();
    this->save_state_();
  }
  ClimateTraits traits() override {
    // The capabilities of the climate device
    auto traits = climate::ClimateTraits();
    traits.set_supports_current_temperature(true);

    traits.set_supported_modes({climate::CLIMATE_MODE_OFF,
                                climate::CLIMATE_MODE_COOL,
                                climate::CLIMATE_MODE_AUTO,
                                climate::CLIMATE_MODE_FAN_ONLY});

    traits.set_visual_min_temperature(TEMP_MIN);
    traits.set_visual_max_temperature(TEMP_MAX);
    traits.set_visual_temperature_step(TEMP_STEP);

    return traits;
  }
};

This code when configured in the YAML for ESPHome sends IR signals to my AC and allows me to control it with a sweet interface.

AC control on my iPhone
AC control in HASS web interface

Building my own Alexa smart home skill to control my IoT devices

At this point I can control my IoT devices with the Home App. In my previous attempt described in my last post, I used Sinric to control my IoT devices with Alexa. This had a couple of problems, first I couldn’t control them with the Home App and their were some minor inconveniences that I had no control over. For example, the TV needed to be first turned on with Alexa to have control over TV controls such as volume change and pause/play with Alexa. But this was a big issue because if the TV was turned on by remote, which it often was Sinric was not able to detect that it was on. I noticed this when I tried to “skip ad” with Alexa and it didn’t work.

Home Assistant does have a built-in integration which can allow you to connect to Alexa, but this had a monthy subscription cost. By making my own Alexa smart home skill I can better customize this system and it would cost me at most a few cents a year.

So now I had to make a smart home skill, to have full control and configuration my IoT devices. My diagram best visualizes how Alexa connects with the Home Assistant on my Raspberry Pi, because I had to use various services within the AWS cloud.

First, I have my smart home skill added to Alexa Account. When I give Alexa a command like “Alexa, turn on the light”, the smart home skill turns this voice command into a JSON object and triggers a AWS lambda function that I programmed in Python. The lambda function processes the JSON object and returns a response to the smart home skill.

When the lambda function gets the JSON object with data telling it to turn on the light, the code checks if that smart device is already on from its state stored in the AWS DynamoDB database. Then only does it change the state (turn on the light). It does this by sending an MQTT message through AWS IoT core to HASS.

I used AWS IoT core to setup a secure connection between the AWS cloud and the MQTT integration in Home Assistant. There are certificates stored in the Raspberry Pi for the secure connection. To control a IoT device the lambda function sends MQTT messages to an IoT topic to HASS in JSON format.

To process MQTT messages and complete the appropriate action (turning on the light) I have setup automations on HASS. Every time an IoT device’s state changes HASS will send an MQTT message back to AWS IoT core. This message is processed by an IoT rule I setup which changes the state of the device in the AWS DynamoDB database. This ensures that all state changes from the Home App or Alexa are always in sync.

The code for Alexa skill lambda function is given by Amazon, but I decided to alter it make it easier for me to understand and later configure, because it was kind of confusing for me. But, I do understand there must have been a reason they programmed it the way they did.

Also I used AWS IAM to setup permission for different AWS services to talk to each other. This makes sure that each service has access to only the necessary functions of another AWS service. This makes the overall system more secure.

Final Thoughts

Now, everyone at my home has quick access to the lights or AC from their phone and we can also do the same with Alexa. In fact, we control the smart devices from their phone more than Alexa. So this project overall was a great success!

Here’s how the controls look like on the Home Assistant web interface:

And here’s how it looks like from the Home App:

Home App on my iPhone
Control of a light