Create Custom IR remote w/ Home Assistant & ESPHome

Create Custom IR remote w/ Home Assistant & ESPHome

I had several nagging reasons to create a single universal remote that was controllable from an app on our phones:

  • I was looking for a cool Home Assistant project, plus a project to use up some of the extra ESP8266’s I had
  • We had way too many IR remotes in our living room
  • We only ever pressed one or two button per remotes
  • Setting up for the evening using said remotes, was a juggle
  • Remotes were always stashed high and far from kids, or lost in couch cushions
  • Related to above, many of the batteries in the remotes are dangerous to small kids if somehow broken/opened
  • We always had our phones on us, but remotes were always out of reach or forgotten, always after getting comfy

Here’s what I did to build a custom universal remote that replaced all my IR remotes including my AppleTV touch remote:


  1. HomeAssistant setup & running (mines on a rpi3b)
  2. HomeAssistant mobile App
  3. ESP8266 (or ESP32) w/ micro usb cable
  4. IR LED and IR Receiver
  5. NPN transistor (BC337)
  6. Jumper cables, or wire + soldering stuff
  7. Heat shrink
  8. Optionally for installation: long micro-usb to usb A cable, 3d printed case, Velcro to stick case to wall

Optional: Setup Apple TV to use IR

Not totally sure what interface the Apple Remote uses, but if looking to Replace Apple TV: you can map the Apple TV to listen to some arbitrary button presses from any unused remote following this doc.

Build an IR Receiver to Capture Codes

First step is to capture all the IR codes. I set up a receiver as follows:

The nice part about this layout for the IR receiver I had is I was able to just slip it into a breadboard, no wiring:

In the ESPHome section of Home Assistant:

  1. Click the big “+” add button in bottom
  2. Fill out the wizard
  3. Once created, edit the module and append to the bottom of the config:
    number: GPIO14
    inverted: True
    mode: INPUT_PULLUP
  dump: raw
  idle: 25ms

For reference, mine in full looks like:

  name: ir_receiver
  platform: ESP8266
  board: nodemcuv2
  ssid: !secret wifi_name
  password: !secret wifi_pass
    ssid: "Ir Receiver Fallback Hotspot"
    password: !secret irfallback
  password: !secret ota_pass
  password: !secret ota_pass

    number: GPIO14
    inverted: True
    mode: INPUT_PULLUP
  dump: raw
  idle: 25ms

Then validate, and upload through the USB port if this is the first chunk of code being sent, after that, no need to be plugged in and Over The Air works fine. Home Assistants Notification panel should alert of a new device detected, and you can add in the device and the entity.

Once uploaded, the logs should continue to show in the modal window. Try pointing an IR remote at it and smashing some buttons. If it yells a bunch of crazy number strings at you: it’s working!

There’s some important notes while looking to record a single button:

  • if the “Received Raw” starts with a negative number, you pin may not be inverted and may need to be. IR signals are a positive number (length of high value), followed by a negative number (length of low value).
  • A single press resulted for me in 3 lines. I needed to manually merge the first two and ignore the third. Why? I have no idea. For example, here’s the power button for my TV:
[20:13:30][D][remote.raw:028]: Received Raw: 8992, -4452, 619, -541, 598, -1639, 621, -1663, 598, -540, 598, -540, 597, -541, 598, -541, 597, -1641, 620, -1640, 620, -541, 596, -1640, 621, -541, 597, -541, 597, -541, 598, -540, 598, -541, 597, -1639, 621, -1640, 620, -1640, 620, 
[20:13:30][D][remote.raw:041]:   -1641, 620, -541, 597, -541, 598, -540, 598, -540, 598, -540, 598, -540, 598, -541, 597, -541, 597, -1640, 620, -1640, 621, -1641, 619, -1640, 621
[20:13:30][D][remote.raw:041]: Received Raw: 8993, -2205, 621

I combine the first and second lines, and remove the last line. The need to remove the third comes from line 2 ending in a positive, and line 3 starting in a positive, IR patterns must be positive negative. I don’t know why that third line is there, but removing it everything still worked in the end. From the above, my IR signal for the TV power button ended up being:

8992, -4452, 619, -541, 598, -1639, 621, -1663, 598, -540, 598, -540, 597, -541, 598, -541, 597, -1641, 620, -1640, 620, -541, 596, -1640, 621, -541, 597, -541, 597, -541, 598, -540, 598, -541, 597, -1639, 621, -1640, 620, -1640, 620, -1641, 620, -541, 597, -541, 598, -540, 598, -540, 598, -540, 598, -540, 598, -541, 597, -541, 597, -1640, 620, -1640, 621, -1641, 619, -1640, 621

Creating the IR Blaster

With the codes captured, next step is to build a transmitter. I think it’s best to use a dedicated board for sending, instead of adding the blaster to the receiver. I originally tried to setup just one board that would both receive and sent the codes, but found the boards lacked the brains to push and get at the same time successfully, which made debugging nearly impossible. I found it far easier to dedicate one device to just receiving and another to sending.

I read that ESP8266’s weren’t that great at sending the blasts and ESP32’s were a better choice due to some improved chips onboard, however I had success with ESP8266.

I wired the blaster like so:

The just like before, created a new ESPHome entry with the following appended to the config, adding the in the buttons and their codes:

  # D2
    number: GPIO4
  carrier_duty_percent: 50%
  - platform: template
    id: tv_power_btn
    name: TV Power Button
      - remote_transmitter.transmit_raw:
          carrier_frequency: 38kHz
          code: [8995, -4449, 623......-1638, 622, -1639, 622]
  - platform: template
    name: TV Up Button
    id: tv_up_button
      - remote_transmitter.transmit_raw:
          carrier_frequency: 38kHz
          code: [8995, -4447, 625......-1637, 623, -1637, 624]
  - platform: template
    name: TV Left Button
    id: tv_left_button
      - remote_transmitter.transmit_raw:
          carrier_frequency: 38kHz
          code: [8937, -4506, 566......-1671, 590, -1693, 567]


LoveLace Remote Interface

I saw some articles about using a tiles plugin for remote, but I reckoned I could get away using Button cards in a Vertical and Horizontal cards to create a table for button cards. To do this, I:

  1. Created a vertical group for the remote
  2. Created a horizontal group for a row of buttons
  3. Created more adjacent horizontal groups for the following rows
  4. I created the buttons by selecting a Button Card
    1. selecting the entity
    2. Getting a cool icon from
    3. Setting “show name” to off
    4. Setting Icon Height to 50

Here’s a peak at my setup in it’s .yaml form:

type: vertical-stack
  - type: horizontal-stack
      - type: button
          action: toggle
        entity: switch.insignia_tv_power_button
        name: TV
        icon: 'mdi:power-standby'
        icon_height: 50px
        show_name: false
      - type: button
          action: toggle
        entity: switch.tv_nothing_button
        show_name: false
        show_icon: false
      - type: button
  - type: horizontal-stack
      - type: button

Which I laid out to look like:

An important one here is the tv_nothing_button , it is what is says, it’s a button that just like all the others is registered on the esphome setup, however, it sends a invalid value, triggering nothing, but allows the UI to house proper spacing and a consistent look.

Update: looks like the grid card was just released in 0.118 to make this even easier!

Installation Placement

All our devices and lights to control with IR are on a single wall, so mounting the on the opposite wall, higher up towards the roof above our curtains gave the blaster direct line of sight to all devices, and kept it out of the way.

Before mounting I tested all functionality, walking around with the ESP8266 connected to a battery pack to find just the right spot, and quicker debugging.


Most of the designs for esp8266 cases on were too small or oriented the wrong way for this single LED wall mount use. So I designed a custom case with I designed a case for both small and large styles of ESP8266 as I had both, and also did it in a “dead bug” style with lots of room for the bulky dupont connections.

The case can be downloaded here:

Design in shapr3d

Everything Together

Considering for each button press, the flow data is Home Assistant App on Phone > Wifi > Home Assistant > Wifi > ESP8266 blaster > Target Device, it actually works really fast. No noticeable latency or delays connecting. Additionally, I just used the normal HA ESPHome API, not MQTT. Every time I open the app, the module is already connected and ready to press a button (no load or unavailability).

Our families use case for the buttons we needed, is simple and straight forward, so this solution of just firing up the Home Assistant App on our phones was the perfect fit.

Automation & Shortcuts

With all the button entities added into Home Assistant, scripts and automations can be made with ease. For example, I created a “boom button” script that toggles all the power buttons.

Additionally, the Shortcuts app on iOS with the Home Assistant app allows you to turn entities on and off, or run scripts. The title of these shortcuts are pre-wired into Siri, so “Turn TV On” voice commands can be wired up in just a few minutes. More about this here.

I did not store the state (“on/off”) of a button in Home Assistant. Though the button entity can, doing so raised risk of state being incorrect if any of the devices are toggled manually or via the real remotes. I opted it was best to treat the buttons just like an IR remote that are unaware of a device being on or off. This limited automation (eg: turn on if not on), however I’m okay with that.


  1. Thank you so much for the writeup, was painless to follow and got me up and running in no time. did you ever get around to uploading the stl for the case?

  2. Hi – great tutorial, beautifully written – know it’s a skill to balance enough detail with too much detail!

    Just a bit stuck at the final hurdle, created a transmitter as you’ve described, all validates fine and can see it running in the ESPHome log, just can’t see anything in the Home Assistant developer tools/lovelace button card that matches my template name.

    Am I missing something silly here? ie do i need to add anything to the Home Assistant configuration.yaml or something? Tried restarting server also.

    – platform: template
    id: tv_mount_open
    name: TV Mount Open
    – remote_transmitter.transmit_raw:
    carrier_frequency: 38kHz
    code: [20642, -21005, 15500]

    – platform: template
    id: tv_mount_close
    name: TV Mount Close
    – remote_transmitter.transmit_raw:
    carrier_frequency: 38kHz
    code: [20669, -10471, 5169, -10499, 5172]

    1. Thanks!

      After creating the ESPHome node, did you see a Home Assistant notification detecting the new Integration and follow the wizard to add it in? There should be a ESPHome section in the `/config/integrations` page with your modules once they’ve been integrated into Home Assistant- might try poking around there.

  3. Ok, ignore above – apart from my compliments regarding your tutorial! Figured it out – just needed to go into Home Assistant > Integrations and click configure on the component that appeared. Might be worth adding to the tutorial for any other numbskulls out there like me…!

    My final hurdle – I have a TV Mount that’s motorised and I’ve got the command for it to open working perfectly, but the command for close is strange:
    [14:44:42][D][remote.raw:041]: Received Raw: 20644, -21005, 15500
    [14:44:42][D][remote.raw:041]: Received Raw: 20651, -20997, 15496
    I can’t just paste the first couple of rows as it complains that it needs to alternate positive and negative numbers. Likewise one row doesn’t work – any ideas how to combine these two rows so it’ll work??

    1. Hm, it’s strange that the CLOSE code you just posted shares the same signal as the OPEN one (`20644, -21005, 15500`). Also noting that of those two codes last commented, the number are very very close, the biggest diff is only off by 8, so I think the CLOSE pasted above is basically the same signal, it just sent twice. I’m not sure why these are basically the same as the OPEN signal. It’s strange.

      1. Got this working randomly – just repeated a few times and eventually got a 5 set code which works perfectly. Very strange, but now it’s saved it works perfectly…

        Now looking at automating my garage door keys also with this!

        PS have an RF transmitter for my oil tank level:

        Would I need to change any components for this to work or is it just a simple as changing the code in ESPHome to use RF?

        Great tutorial again – been really useful!

  4. Great post, David! Exactly what I was looking for.
    That will be my next step in automating a bunch of things at home.
    Before I start, just two questions out of curiosity:
    1- Do you think this “carrier_frequency: 38kHz” would work for any equipment? (many brands, many ages, many kinds…)
    2- Do you see any problem in putting 2 or more emitter LEDs in parallel? (if OK, which transistor would you suggest?) In my case, the things to be automated are rather scattered, then I would need more than one line to fire all of them.

  5. Hi fabulous tutorial, just what i need.
    I am using ESP8266 also.
    While messing around with my setup and trying to figure this out i tried reading the codes both on the ESPHome setup and a Arduino based system to compare them. The Arduino library seems to handle the reads better and also gives the Address, Command and Data decoded too.
    I noted that the last numbers in the raw numbers on the Arduino read were significantly different to the previous ones; …..628, 1618, 634, 1618, 630, …..40954, 9202. The Arduino output doesn’t add the -ve to the numbers so it is the small numbers that are the -ve.
    The -ve’s being actual the low pulse signal.
    The large -ve number at the end is i suspect (a guess) a long pulse to indicate the end of the transmission. The Arduino library seems to handle this while the ESPHome one doesn’t recognise it as such and thus creates the third set of numbers.
    I hope the above makes some sense, just trying to answer your question why you needed to drop the last set of numbers.
    Your ESPHome information is invaluable as this YAML stuff is a black art to me.

  6. My receiver gave me a block of code and it may have been picking up lights or something because that log was going by fast. Even though I had that issue, I managed to get my window A/C unit into HA. I managed to fit it all into an old glass break sensor from the previous owners’ alarm system. That sensor had a hole the perfect size for the IR LED.

    Thanks for this write up.

  7. Hi David

    Thanks for your excellent guide!
    Finally I’m able to be lacy and don’t have to leave the couch to turn on my receiver (hidden behind a door).
    About the code i had to use the third row to control my Denon.

    And one last thing, did you glue the IR och did you print a custom plastic ring that fits into the spacers on the lid?

  8. Total novice to home automation and HA/esphome here – so maybe this is a weird ask. But, if someone has a fully working IR repeater already set up, could a wired output be established?

    My concept would just be that instead of the ‘blaster’ side doing the emitting of IR signals, it would instead send the IR codes over 3.5mm stereo lines to the input of my established repeater.

Leave a Reply

Your email address will not be published. Required fields are marked *