HDMI-CEC ESPHome Component

An ESPHome component that supports receiving and transmitting HDMI-CEC messages to connected HDMI devices. The ultimate goal of this project is to eventually be merged into the core ESPHome project once it’s up to quality. It’s currently out for review as esphome/esphome#3017 (see also: esphome/esphome-docs#1789).

The core CEC driver is forked from s-moch/CEC.

My use case: I already have an IR blaster built with ESPHome, but my new TCL TV has a Bluetooth remote. I want to control my older sound gear (connected over optical) with the TV remote but this TV only supports controlling sound gear via HDMI (typically for HDMI-ARC devices). This component allows me to intercept HDMI-CEC volume commands and transmit the IR codes to control the soundbar. It also allows my Apple TV to control the soundbar (so I can control volume from the Remote app on my phone ?). Theoretically it should allow you to make any older non-HDMI equipment work seamlessly with newer gear. You can also do things like monitor which source is selected, which HDMI devices are powered on, etc.

Drop me a line on Twitter or the ESPHome Discord if you end up trying it and/or have ideas for cool use cases!


Since HDMI-CEC is a 3.3V protocol, no external components are needed. A HDMI breakout connector might be handy though. Just connect:

  • GPIO of your choice to HDMI pin 13 (the examples use GPIO4)
  • GND from the ESP board to HDMI pin 17 (DDC/CEC Ground)
  • 5V from the ESP board to HDMI pin 18 (typtically how the HDMI device knows something is connected)


For now on the ESP8266, until the HDMI driver is rewritten to fully use interrupts, it’s a good idea to bump the CPU speed to 160MHz:

    board_build.f_cpu: 160000000L

Add this repo to your external components

  - source: github://johnboiles/esphome-hdmi-cec

Configure your hdmi_cec component. The on_message triggers will only fire if any specified source, destination, opcode, and data match. Something like this (the below is abbreviated — see full example here).

  # The initial logical address -- corresponds to device type. This may be
  # reassigned if there are other devices of the same type on the CEC bus.
  address: 0x05 # Audio system
  # Promiscuous mode can be enabled to allow receiving messages not intended for us
  promiscuous_mode: false
  # Typically the physical address is discovered based on the point-to-point
  # topology of the HDMI connections using the DDC line. We don't have access
  # to that so we just hardcode a physical address.
  physical_address: 0x4000
  pin: 4 # GPIO4
    - opcode: 0xC3 # Request ARC start
        - hdmi_cec.send: # Report ARC started
            destination: 0x0
            data: [ 0xC1 ]
    - opcode: 0x71 # Give audio status
      source: 0x0 # From the TV
        - hdmi_cec.send:
            destination: 0x0
            data: [ 0x7A, 0x7F ]
    - opcode: 0x46 # Give OSD name
        - hdmi_cec.send:
            destination: 0x0
            data: [0x47, 0x65, 0x73, 0x70, 0x68, 0x6F, 0x6D, 0x65]
    - data: [0x44, 0x41] # User control pressed: volume up
        - logger.log: "Volume up"

You can use the hmdi_cec.send action from anywhere you can use actions.

  - platform: template
    name: TV Turn On
      - hdmi_cec.send:
          source: 0x05 # Optional since we'll default to `hdmi_cec.address`
          destination: 0x0
          data: [ 0x04 ]


  • The timing for receiving and parsing CEC messages depends on the timing with which loop is called and thus this is sensitive to other things the microcontroller are doing that may delay the loop method getting called. I’ve implemented pin change interrupts which makes receive more reliable but more work needs to be done to make this entirely interrupt driven. The ESP32 will probably work better as-is, though I haven’t tried it yet.


  • Adapt or rewrite the CEC driver to be fully interrupt-driven.
  • Add GitHub action to test the build
  • Write tests?
  • Fritzing (or similar) connection diagram

See Also


View Github