MCPcopy Create free account
hub / github.com/Palakis/esphome-native-hdmi-cec

github.com/Palakis/esphome-native-hdmi-cec @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
86 symbols 126 edges 5 files 9 documented · 10% updated 30d ago★ 3321 open issues

Browse by type

Functions 76 Types & classes 10
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

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

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

The video below demonstrates how to connect an HDMI breakout board to an ESP32 dev board:

https://github.com/user-attachments/assets/71e847da-4b04-443f-9d97-f764fa97a007

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 be 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 ESPHome 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 (just delete what you don't need):

```yaml 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(cec_destination);" data: !lambda "std::vector charVector; for (int i : cec_data) { charVector.push_back(static_cast(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 be 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 addressed 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:

Core symbols most depended-on inside this repo

Shape

Method 71
Class 7
Function 5
Enum 3

Languages

C++95%
Python5%

Modules by API surface

components/hdmi_cec/hdmi_cec.h39 symbols
components/hdmi_cec/cec_decoder.cpp20 symbols
components/hdmi_cec/hdmi_cec.cpp16 symbols
components/hdmi_cec/cec_decoder.h7 symbols
components/hdmi_cec/__init__.py4 symbols

For agents

$ claude mcp add esphome-native-hdmi-cec \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page