Skip to content

DIYtechie/esphome-native-hdmi-cec

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ESPHome Native HDMI-CEC Component

Make your ESPHome devices speak the (machine) language of your living room with this native HDMI-CEC (Consumer Electronics Control) component!

Features

  • Native CEC 1.3a implementation
    • Implemented from scratch specifically for this component. No third-party CEC library used.
    • Meant to be as simple, lightweight and easy-to-understand as possible
    • Interrupts-based receiver (no polling at all). Handles low-level byte acknowledgements
  • Receive CEC commands
    • Handle incoming messages with on_message triggers
      • Each trigger specified in on_message supports filtering based on source, destination, opcode and/or message contents
    • Built-in handlers for some of the system commands defined in the spec :
      • "Get CEC Version"
      • "Give Device Power Status"
      • "Give OSD Name"
  • Send CEC commands
    • Built-in hdmi_cec.send action

To-do list

  • Automatic Physical Address Discovery through E-DDC

Getting Started (Quick Overview)

Step 1: Connect the hardware

Connect the microcontroller to an HDMI connector (HDMI connectors and breakout boards can be found on Amazon and AliExpress)

HDMI Pin Connect to Microcontroller pin
13 (CEC Data Line) => Any input/output GPIO (e.g., GPIO26)
17 (CEC Ground) => Ground
18 (+5V (optional)) => 5V

How to connect HDMI to ESP32

The HDMI breakout board shown is this model on AliExpress.

CEC uses 3.3V logic – safe for ESP32/ESP8266 (or any other microcontroller with 3.3V logic).

Step 2: Set up ESPHome

  • Start by creating your device using ESPHome Device Builder (e.g., via Home Assistant’s ESPHome Add-on or ESPHome Web).
  • Once your device is created, click "Edit" to access the YAML configuration.
  • (If using an ESP32-C3, it’s recommended to use type: esp-idf)

Step 3: Add the component

In your ESPhome YAML configuration, add this Git repository as an external component (e.g. below captive portal):

external_components:
  - source: github://Palakis/esphome-hdmi-cec

Step 4: Basic HDMI-CEC Setup

Add the hdmi_cec: block:

hdmi_cec:
  # Pick a GPIO pin that can do both input AND output
  pin: GPIO26 # Required
  
  # The address can be anything you want. Use 0xF if you only want to listen to the bus and not act like a standard device
  address: 0xE # Required
  
  # Physical address of the device. In this case: 4.0.0.0 (HDMI4 on the TV)
  # DDC support is not yet implemented, so you'll have to set this manually.
  physical_address: 0x4000 # Required
  
  # The name that will we displayed in the list of devices on your TV/receiver
  osd_name: "my device" # Optional. Defaults to "esphome"
  
  # By default, promiscuous mode is disabled, so the component only handles directly-address messages (matching
  # the address configured above) and broadcast messages. Enabling promiscuous mode will make the component
  # listen for all messages (both in logs and the on_message triggers)
  promiscuous_mode: false # Optional. Defaults to false
  
  # By default, monitor mode is disabled, so the component can send messages and acknowledge incoming messages.
  # Enabling monitor mode lets the component act as a passive listener, disabling active manipulation of the CEC bus.
  monitor_mode: false # Optional. Defaults to false

You now have a functioning CEC receiver.


➕ Optional Features

All of the following are optional – include only what you need.


1. React to Incoming Messages

Add under hdmi_cec::

hdmi_cec:
  ...
  on_message:
    - opcode: 0x36  # "Standby"
      then:
        logger.log: "Received standby command"
    
    # Respond to "Menu Request" (not required, example purposes only)
    - opcode: 0x8D
      then:
        hdmi_cec.send:
          # both "destination" and "data" are templatable
          destination: !lambda return source;
          data: [0x8E, 0x01] # 0x01 => "Menu Deactivated"

You can filter by:

  • "source": match messages coming from the specified address
  • "destination": match messages meant for the specified address
  • "opcode": match messages bearing the specified opcode
  • "data": exact-match on message content

If no filter is set, you will catch all messages.


2. Add Template Buttons to Send CEC Commands

Add a button: section to create UI buttons:

button:
  - platform: template
    name: "Turn TV Off"
    on_press:
      hdmi_cec.send:
        destination: 0
        data: [0x36]

More button examples in the advanced EHPHome configuration example below.


3. Enable CEC Commands via Home Assistant Services

Under api::

api:
  services:
    - service: hdmi_cec_send
      variables:
        cec_destination: int
        cec_data: int[]
      then:
        - hdmi_cec.send:
            destination: !lambda "return static_cast<unsigned char>(cec_destination);"
            data: !lambda |-
              std::vector<unsigned char> vec;
              for (int i : cec_data) vec.push_back(static_cast<unsigned char>(i));
              return vec;

4. Get Raw and Human-readable CEC Messages as Home Assistant Events

When any HDMI-CEC message is received, send a Home Assistant event called "esphome.hdmi_cec". The event contains both the raw hexadecimal frame and a human-readable translation. These events are ideal for automations because they can be directly used as triggers in Home Assistant. They are also useful for debugging (visible under Developer Tools → Events) and do not consume space in Home Assistant’s database.

Add this under hdmi_cec::

hdmi_cec:
  ...
  on_message:
    
    - then:
        - homeassistant.event:
            event: esphome.hdmi_cec  # Home Assistant event type (visible in Developer Tools → Events)
            data:
              source: !lambda 'return source;'   # Logical address of the device that sent the message
              destination: !lambda 'return destination;'  # Logical address of the target device
              opcode: !lambda 'return data.size() ? data[0] : 0;'  # First byte of data = command opcode
              raw: !lambda 'return hdmi_cec::Frame(source, destination, data).to_string(true);'  # Full frame in hex (e.g. "40:36")
              translated: !lambda 'return hdmi_cec::Frame(source, destination, data).to_string();'  # Human-readable form (e.g. "TV → Broadcast: Standby")

Text sensors (for dashboards or easier debugging)

If you prefer to monitor messages directly in a dashboard or entity list, you can also expose decoded CEC messages as text sensors. This may make debugging easier because you can see the latest messages without switching to Developer Tools. However, keep in mind that text sensors persist their state in Home Assistant’s database and may slightly increase database size over time.

Add this under hdmi_cec::

hdmi_cec:
  ...
  on_message:
      
      #CEC message decoder (human-readable translation)
    - then:
        - lambda: |-
            std::string translated = hdmi_cec::Frame(source, destination, data).to_string();
            id(cec_translated_message).publish_state(translated);

And add two text_sensor: blocks (required):

text_sensor:
  - platform: template
    name: "HDMI CEC Raw Message"
    id: cec_raw_message
    update_interval: never

  - platform: template
    name: "HDMI CEC Translated Message"
    id: cec_translated_message
    update_interval: never

Consider excluding these sensors from your Home Assistant database to save space. If MQTT is enabled, the text sensor values (raw and translated) will also be sent via MQTT


5. Publish CEC Messages over MQTT (CEC-O-MATIC format)

Under mqtt: and hdmi_cec::

mqtt:
  broker: '192.168.1.100' # insert IP or DNS of your own MQTT broker (e.g. the IP of your HA server)
  username: !secret mqtt_user # make sure your MQTT username is added to the secrets file in the ESPHome Add-on
  password: !secret mqtt_password # make sure your MQTT password is added to the secrets file in the ESPHome Add-on
  discovery: false # if you only want your own MQTT topics

hdmi_cec:
  ...
  promiscuous_mode: true
  on_message:
    - then:
        mqtt.publish:
          topic: cec_messages
          #Payload in CEC-O-Matic format
          payload: !lambda |-
            return hdmi_cec::Frame(source, destination, data).to_string(true);

Advanced Example (All Features Combined)

Here’s a full YAML snippet that includes all optional features together:

esphome:
  name: hdmi-cec-bridge
  friendly_name: HDMI CEC Bridge

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "..."
  
  services:
    - service: hdmi_cec_send
      variables:
        cec_destination: int
        cec_data: int[]
      then:
        - hdmi_cec.send:
            destination: !lambda "return static_cast<unsigned char>(cec_destination);"
            data: !lambda "std::vector<unsigned char> charVector; for (int i : cec_data) { charVector.push_back(static_cast<unsigned char>(i)); } return charVector;"

ota:
  - platform: esphome
    password: "..."

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "HDMI CEC Fallback Hotspot"
    password: "..."

mqtt:
  broker: '192.168.1.100' # insert IP or DNS of your own MQTT broker (e.g. the IP of your HA server)
  username: !secret mqtt_user # make sure your MQTT username is added to the secrets file in the ESPHome Add-on
  password: !secret mqtt_password # make sure your MQTT password is added to the secrets file in the ESPHome Add-on
  discovery: false # if you only want your own MQTT topics

captive_portal:

external_components:
  - source: github://Palakis/esphome-hdmi-cec

hdmi_cec:
  # Pick a GPIO pin that can do both input AND output
  pin: GPIO10 # Required
  
  # The address can be anything you want. Use 0xF if you only want to listen to the bus and not act like a standard device
  address: 0xE # Required
  
  # Physical address of the device. In this case: 4.2.0.0 (The ESP32 is plugged into HDMI 2 on the receiver which is plugged into HDMI4 on the TV)
  # DDC support is not yet implemented, so you'll have to set this manually.
  physical_address: 0x4200 # Required
  
  # The name that will we displayed in the list of devices on your TV/receiver
  osd_name: "HDMI Bridge" # Optional. Defaults to "esphome"
  
  # By default, promiscuous mode is disabled, so the component only handles directly-address messages (matching
  # the address configured above) and broadcast messages. Enabling promiscuous mode will make the component
  # listen for all messages (both in logs and the on_message triggers)
  promiscuous_mode: true # Optional. Defaults to false
  
  # By default, monitor mode is disabled, so the component can send messages and acknowledge incoming messages.
  # Enabling monitor mode lets the component act as a passive listener, disabling active manipulation of the CEC bus.
  monitor_mode: false # Optional. Defaults to false

  on_message:

  # Send CEC messages as Home Assistant events
  - then:
      - homeassistant.event:
          event: esphome.hdmi_cec # Home Assistant event type (visible in Developer Tools → Events)
          data:
            source: !lambda 'return source;'        # Logical address of the device that sent the message
            destination: !lambda 'return destination;'  # Logical address of the target device
            opcode: !lambda 'return data.size() ? data[0] : 0;'  # First byte of data = command opcode
            raw: !lambda 'return hdmi_cec::Frame(source, destination, data).to_string(true);'  # Full frame in hex (e.g. "40:36")
            translated: !lambda 'return hdmi_cec::Frame(source, destination, data).to_string();'  # Human-readable form (e.g. "TV → Broadcast: Standby")

  # Send CEC messages via MQTT in CEC-O-Matic format
  - then:
      - mqtt.publish:
          topic: cec_messages
          payload: !lambda |-
            return hdmi_cec::Frame(source, destination, data).to_string(true);

  # Publish decoded CEC messages as text sensors (raw and translated)
  - then:
      - lambda: |-
          hdmi_cec::Frame frame = hdmi_cec::Frame(source, destination, data);
          id(cec_raw_message).publish_state(frame.to_string(true));
          id(cec_translated_message).publish_state(frame.to_string());

text_sensor: #Consider excluding these sensors from your Home Assistant database to save space.
  - platform: template
    name: "HDMI CEC Raw Message"
    id: cec_raw_message #Do not delete if used with CEC message decoder
    update_interval: never

  - platform: template
    name: "HDMI CEC Translated Message"
    id: cec_translated_message #Do not delete if used with CEC message decoder
    update_interval: never


# Example button configuration for common HDMI-CEC commands
# ----------------------------------------------------------
# The examples below use Apple TV (playback device 1) and PlayStation 4 (playback device 2)
# as references for typical CEC playback devices. 
# Other devices may use different command opcodes — 
# refer to your device’s CEC documentation if the examples do not work as expected.

button:
  - platform: template
    name: "Turn all HDMI devices off"
    on_press:
      hdmi_cec.send:
        destination: 0xF  # Broadcast
        data: [0x36]      # "Standby" opcode

  - platform: template
    name: "Turn TV on"
    on_press:
      hdmi_cec.send:
        source: 1
        destination: 0
        data: [0x04]      # Works with Samsung TVs. For LG, try [0x0D]; for Sony, [0x44, 0x6D].

  - platform: template
    name: "Turn TV off"
    on_press:
      hdmi_cec.send:
        source: 1
        destination: 0
        data: [0x36]

  - platform: template
    name: "Volume up"
    on_press:
      hdmi_cec.send:
        destination: 0x5  # Usually the Audio System
        data: [0x44, 0x41]

  - platform: template
    name: "Volume down"
    on_press:
      hdmi_cec.send:
        destination: 0x5
        data: [0x44, 0x42]

  - platform: template
    name: "Mute"
    on_press:
      hdmi_cec.send:
        destination: 0x5
        data: [0x44, 0x43]

  # --- Playback Device 1 (Apple TV example) ---
  - platform: template
    name: "Turn on Playback device 1"
    on_press:
      hdmi_cec.send:
        destination: 4     # Typical address for first playback device (Apple TV)
        data: [0x44, 0x6D] # "Power On Function" / "Play"

  - platform: template
    name: "Turn off Playback device 1"
    on_press:
      hdmi_cec.send:
        destination: 4
        data: [0x36]       # "Standby"

  - platform: template
    name: "Playback 1 Home"
    on_press:
      hdmi_cec.send:
        destination: 4
        data: [0x44, 0x09] # "Root Menu"

  - platform: template
    name: "Playback 1 Select/OK"
    on_press:
      hdmi_cec.send:
        destination: 4
        data: [0x44, 0x00] # "Select"

  - platform: template
    name: "Playback 1 Back/Exit"
    on_press:
      hdmi_cec.send:
        destination: 4
        data: [0x44, 0x0D] # "Exit"

  - platform: template
    name: "Playback 1 Play/Pause"
    on_press:
      hdmi_cec.send:
        destination: 4
        data: [0x44, 0x44] # "Play" / "Pause Toggle"

  # --- Playback Device 2 (PlayStation 4 example) ---
  - platform: template
    name: "Turn on Playback device 2"
    on_press:
      hdmi_cec.send:
        destination: 8     # Typical address for second playback device (PS4)
        data: [0x44, 0x6D] # May vary depending on firmware

  - platform: template
    name: "Turn off Playback device 2"
    on_press:
      hdmi_cec.send:
        destination: 8
        data: [0x36]

  - platform: template
    name: "Playback 2 Play/Pause"
    on_press:
      hdmi_cec.send:
        destination: 8
        data: [0x44, 0x46]

3D printed case (ESP32-C3 SuperMini)

If you’re using an ESP32-C3 SuperMini, you can 3D-print a dedicated case designed by DIYtechie on MakerWorld.

ESP32-C3 HDMI case

The case is optimized for the ESP32-C3 SuperMini form factor and designed to fit all the required components (including thee HDMI sockets) referenced) in the smallest possible footprint. Follow MakerWorld link for detailed description and bill of materials.


✅ Compatibility

Platform Supported Notes
ESP32 Fully supported (use type: esp-idf for ESP32-C3
ESP8266 Tested and works
RP2040 Tested and works
LibreTiny Not supported

About

HDMI-CEC implementation for ESPHome

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C++ 89.2%
  • Python 10.8%