CEC volume control for IR devices by pretending to be an HDMI ARC device

I recently got a TCL 635 Roku TV. One annoyance with it is that the RF remote’s volume buttons are useless unless you are using the TV speakers OR using a receiver/soundbar with HDMI ARC. My soundbar is connected over optical, and I’m not eager to replace it, so I started investigating how to intercept HDMI CEC volume commands to send to my ESPHome IR blaster. One glitch I ran into is that my TV seems to not send CEC volume commands unless you pretend to be an HDMI ARC device. I was able to figure out how to advertise as an ARC audio device, after which my TV would send the proper CEC volume commands. I’m adding my notes here in case it’s helpful to others!

Parts

  • Raspberry Pi + SD card + power supply (I use a Pi Zero W)
  • HDMI cable with CEC pin (some cheaper HDMI cables don’t support CEC)

First flash the Pi with Raspbian Lite and set up wifi and ssh. Then ssh to the pi. The following commands should be run via ssh on the pi.

Get some dependencies

sudo apt-get install libcec-dev build-essential python3-dev python3-pip git

Now clone the python-cec library

git clone https://github.com/trainman419/python-cec.git

Now you need to make a change to python-cec to pretend to be an audio device. (I hope to make a permanent change to python-cec so this won’t be necessary in the future.)

cd python-cec
nano cec.cpp

And make the following change from RECORDING_DEVICE to AUDIO_SYSTEM:

-   CEC_config->deviceTypes.Add(CEC_DEVICE_TYPE_RECORDING_DEVICE);
+   CEC_config->deviceTypes.Add(CEC_DEVICE_TYPE_AUDIO_SYSTEM);

Now build and install python-cec

sudo python3 setup.py install

Also grab the Home Assistant API Python package

pip3 install HomeAssistant-API

Now create a new script

nano ~/cecwatcher.py

And paste my script. You’ll need to change your HA URL, add an access token, and almost certainly change the service call to match the device / method for changing volume in your system. In my case I have an ESPHome IR blaster, that issues commands as part of a universal media_player, in order to control my Vizio soundbar. Modify the contents of the volume_up/volume_down/volume_mute methods based on how your HA config changes volume. You could also just put an IR LED on the Pi itself if you didn’t already have an IR blaster.

import time
import cec
from homeassistant_api import Client

client = Client(
    # Replace with your Home Assistant domain
    'http://10.0.0.42:8123/api/',
    # Create a long-lived access token from your profile page in HA
    'YOURLONGLIVEDACCESSTOKENHERE'
)
# Replace as necessary with your own HA entity and services for volume
entity_id='media_player.soundbar'
def volume_up():
    client.get_domains().media_player.services['volume_up'].trigger(entity_id=entity_id)
def volume_down():
    client.get_domains().media_player.services['volume_down'].trigger(entity_id=entity_id)
def volume_mute():
    client.get_domains().media_player.services['volume_mute'].trigger(entity_id=entity_id, is_volume_muted=True)

def callback(event, *argv):
    if event == cec.EVENT_COMMAND:
        command = argv[0]
        print("command", command)
        if command['opcode'] == cec.CEC_OPCODE_REQUEST_ARC_START:
            print("Reporting ARC started")
            cec.transmit(cec.CECDEVICE_TV, cec.CEC_OPCODE_REPORT_ARC_STARTED, '', cec.CECDEVICE_AUDIOSYSTEM)
    elif event == cec.EVENT_KEYPRESS:
        code, duration = argv
        print("keypress", code, duration)
        if code == 65 and duration == 0:
            volume_up()
            print("volume up")
        elif code == 66 and duration == 0:
            volume_down()
            print("volume down")
        elif code == 67 and duration == 0:
            volume_mute()            
            print("mute")
    else:
        print("event", event, argv)

cec.add_callback(callback, cec.EVENT_ALL & ~cec.EVENT_LOG)
cec.init()

# Sleep forever (CEC stuff will run in the background)
while True:
    time.sleep(100)

Now try running it to see if it works. If it doesn’t try unplugging/replugging your HDMI, turning things off/on, and other HDMI cables (not all cables support CEC)

python3 ~/cecwatcher.py

(use CTRL-C to exit)

You can also create a systemd unit file so this starts automatically when the pi starts

sudo nano /etc/systemd/system/cecwatcher.service

And paste in the following

# systemd unit file for cecwatcher.py

[Unit]
Description=CEC Watcher

[Service]
ExecStart=/usr/bin/python3 /home/pi/cecwatcher.py
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs
Environment=PYTHONUNBUFFERED=1
User=pi

[Install]
# Tell systemd to automatically start this service when the system boots
# (assuming the service is enabled)
WantedBy=default.target

Now start it up and try it out (you might need to turn

sudo systemctl start cecwatcher
sudo systemctl status cecwatcher

And set it up to start on boot

sudo systemctl enable cecwatcher

Now you should have a thing that can receive and intercept CEC volume commands and relay them to HA to control another device! Let me know if you try this and if it works!

3 Likes

Great workaround. I am very keen on recycling hardware, and it would have been so easy just to get a new soundbar.

1 Like

Wait, I’m a bit confused with your setup. Correct me if I am wrong, you connected your RPi to TV to listen for CEC commands then pass them to some “target” device to implement some action. Am I right?

I personally wanted to connect RPi Zero W to my TV and listen for CEC commands to turn on/off my speakers. But I dropped this idea because my TV saw RPi as another video input and the whole setup behaved very weird (TV was switching to RPi when I was turning off other HDMI devices).

Is it possible to setup RPi to be a “receiver” only?

Yep! That’s how it works.

I’m not sure! You can do tvservice --off to turn off hdmi output and there’s probably also some config.txt settings you can set to do the same. I’m not sure if they disable CEC though, it’d be worth experimenting with. I just leave the video output on.

I’m my case my TV only automatically switches to the Pi when the Pi first boots. This does mean I can’t power the Pi from the TV’s usb or the TV will switch to the Pi any time it powers on. But in that configuration I also had trouble convincing the TV that the Pi is an audio device since my script doesn’t run until the Pi fully boots. It would be nice to solve for this since then the Pi could only be powered up when the TV is on and waste less energy. Not a huge priority though as the Pi Zero uses minimal power when not under load.

I think a cleaner solution would be to build an Arduino / ESPHome CEC interface. There’s a little bit of info about this online. Running all Of Linux just to intercept CEC commands seems like overkill.

Thanks a lot, now it is clear.

Seems to me, that the only disadvantage I have will be during RPi boot and if I keep my RPi running all the time there will be no other problems (Initially I planned to power up RPi from my TV, unfortunately, my TV does not have 5V in standby mode, neither my speakers).

BTW, after seeing your post I was trying to refresh my memory about the problem and I found this:

Setting hdmi_ignore_cec_init to 1 will stop the initial active source message being sent during bootup. This prevents a CEC-enabled TV from coming out of standby and channel-switching when you are rebooting your Raspberry Pi. Video options in config.txt

Suppose this should help a bit with the first power on.

1 Like

Oh that’s super helpful!! I’ll try that. Possible it will also fix my other problem too with the tv not thinking the device is an audio device since maybe the first CEC messages the Pi will send will be when my script starts up

Hi @johnboiles
I have an older hisense tv that doesn’t have cec connected to tivo 4k android stick.
Since the tivo only utilize hdmi cec and no ir.
Is there a way to intercept hdmi cec from the tivo?

Do you know if it’s possible to “permanently” change the device type of a Raspberry Pi? I’m wondering if the device type is encoded in the hardware, or if it’s in the system files.

I’ve been struggling to get my setup to act like an “audio system” device. I’ve replicated your procedure, but can’t get my tv to send audio to external speakers. The only way I can get it to do so is via the “cec-client -t a” command. Once I do this, I can see responses to volume up/down, but I can’t really do anything with it.

Probably! I think CEC is just a bus – all devices connected on the HDMI get the same commands through the bus. But I’m not totally sure.

IDK but I would be curious as well!

In general a Pi for this use case is overkill in terms of power / processing. What I think would be better would be an ESPHome device. I wonder if there’s anything out there about how to make a HDMI-CEC interface circuit.

I’ve got a proof-of-concept of this same functionality running on an ESP8266 microcontroller! My goal is to get this into ESPHome and run this from the same device I use for my IRBlaster. That seems like a much simpler solution than running Linux :slight_smile:

Some discussion on the ESPHome Discord:

My janky proof-of-concept code:

Following up here in case folks end up here from Google / forum searches. My ESPHome implementation is done. It’s working great on my TV and I have PRs out for review!

esphome/esphome#3017
esphome/esphome-docs#1789

In the meantime you can use it by adding my repo as an external component. Details about that are in my repo here.

3 Likes

i am trying it out (custom component version)
using an esp32 via hdmi breakout plugged into an input of my TV
i confirm it works on the technical side, i can listen in and send commands.
so far i can send my fireTV to standby, but not my SamsungTV itself.

i am trying a lot of different cec-codes i found by googling.
i have never used cec up until now os i am still learning.

1 Like