An important part of Thuis is integration of our Home Theater system. As the integration is quite extensive and consists of several components, this will be a 3-part blog series. In the first part we start with communicating to CEC-enabled devices from a Raspberry Pi. In the second part we will integrate CEC with the rest of Thuis, and make sure everything works properly together. In the third - and last - part of the Home Theater series we will add the support for Plex.
CEC
Let's start with a short introduction of CEC itself. CEC stands for Consumer Electronics Control and is a feature of HDMI. CEC enables HDMI-devices to communicate with each other. In the ideal situation this means a user only needs one remote control to control all his devices: TV, AV receiver, Blu-ray player, etc. Unfortunately many manufacturers use their own variation of CEC and therefore in a lot of cases one still needs multiple remotes. To get an idea about the protocol have a look a CEC-O-MATIC, this is a great reference for all available commands!
The good news is that the GPU of the Raspberry Pi supports CEC out of the box!
libCEC
To be able to handle the different dialects of CEC, Pulse Eight developed libcec. It enables you to interact with other HDMI devices without having to worry about the communication overhead, handshaking and all the differences between manufacturers. In contrast to what I mentioned in [Pi IoT] Thuis #5: Cooking up the nodes – Thuis Cookbook Raspbian Jessie nowadays provides version 3.0.1 in the Apt repository, so there is no need to use the version from Stretch anymore. I've updated the cookbook accordingly. Other than that provisioning the Raspberry Pi using Chef was straightforward.
libCEC comes with the tool cec-client. This basically gives you a terminal for CEC commands. When we execute cec-client you see it connecting to HDMI and collecting some information about other devices, then we can give it commands. For example we ask it for all devices currently connected with the scan
command:
thuis-server-tv# cec-client -d 16 -t r log level set to 16 == using device type 'recording device' CEC Parser created - libCEC version 3.0.1 no serial port given. trying autodetect: path: Raspberry Pi com port: RPI opening a connection to the CEC adapter... DEBUG: [ 94] Broadcast (F): osd name set to 'Broadcast' DEBUG: [ 96] InitHostCEC - vchiq_initialise succeeded DEBUG: [ 98] InitHostCEC - vchi_initialise succeeded DEBUG: [ 99] InitHostCEC - vchi_connect succeeded DEBUG: [ 100] logical address changed to Free use (e) DEBUG: [ 102] Open - vc_cec initialised DEBUG: [ 105] << Broadcast (F) -> TV (0): POLL // Receiving information from the TV // ... // Request information about all connected devices scan requesting CEC bus information ... DEBUG: [ 41440] << Recorder 1 (1) -> Playback 1 (4): POLL DEBUG: [ 41472] >> POLL sent DEBUG: [ 41473] Playback 1 (4): device status changed into 'present' // ... CEC bus information =================== device #0: TV address: 0.0.0.0 active source: no vendor: Sony osd string: TV CEC version: 1.4 power status: on language: dut device #1: Recorder 1 address: 1.6.0.0 active source: no vendor: Pulse Eight osd string: CECTester CEC version: 1.4 power status: on language: eng device #4: Playback 1 address: 1.1.0.0 active source: yes vendor: Unknown osd string: Apple TV CEC version: 1.4 power status: on language: ??? device #5: Audio address: 1.0.0.0 active source: no vendor: Denon osd string: AVR-X2000 CEC version: 1.4 power status: on language: ??? currently active source: Playback 1 (4)
//
indicates a comment added by me, // ...
indicates output that was hidden as it's not needed for understanding
As you can see currently 4 devices are connected to the bus, including the Raspberry Pi itself (device #1). The Apple TV is the currently active source. You can tell cec-client which output it should give with the -d
parameter. We'll use this for our integration by choosing -d 8
, which just displays the traffic on the bus.
CEC-CDI
To integrate libCEC (or more specifically cec-client) with Java we have to write a wrapper around it. We'll do that in a similar way as MQTT-CDI, so the Java code can observe events happening on the CEC-bus via a CDI observer. I wrote the initial version about a year ago and the full source code is available on my GitHub as Edubits/cec-cdi. It does not support the full CEC protocol yet, but most of the usual commands are available. For example you're able to turn on and off your devices, and send UI commands like play, pause, volume up, etc. You can of course also monitor these same functions, so the app will for example know when you turn off the TV manually.
You can add CEC-CDI to your own project by adding the following dependency to your pom.xml
:
<dependency> <groupId>nl.edubits.cec</groupId> <artifactId>cec-cdi</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
Monitoring what happens in the home theatre system can be done using CDI observers. Currently you can just add a qualifier for the source device, later I might also add some more sophisticated qualifiers such as the type of a command. When you're interesting in all messages send from the TV you can observe them like this:
@ApplicationScoped public class CecObserverBean { public void tvMessage(@Observes @CecSource(TV) Message message) { logger.info("Message received from TV: " + message); } }
To turn the TV on you can send it the IMAGE_VIEW_ON
message without any arguments, for putting it in standby you use the STANDBY
command. In Java this looks as follows:
public class SendExample { @Inject private CecConnection connection; public void send() { // Send message from RECORDER1 (by default the device running this code) to the TV to turn on connection.sendMessage(new Message(RECORDER1, TV, IMAGE_VIEW_ON, Collections.emptyList(), "")); // Send message from RECORDER1 (by default the device running this code) to the TV to turn off connection.sendMessage(new Message(RECORDER1, TV, STANDBY, Collections.emptyList(), "")); } }
ThuisServer-TV
Just like the Core application described in [Pi IoT] Thuis #8: Core v2: A Java EE application, this will be a Java EE application running on WildFly. It includes CEC-CDI. The application itself is quite simple as it's only function is bridging between CEC and MQTT. So we have two @ApplicationScoped
beans observing events.
The CecObserverBean
forwards specific messages from the CEC bus to MQTT. In the example it monitors the power state of the television. Note that my Sony television has its own dialect as well, depending on how the TV is turned off it reports the official STANDBY
command or gives a vendor specific command. When turning on it's supposed to report a certain command as well, but the Sony decides to skip it. That's why - as workaround - I listen to REPORT_PHYSICAL_ADDRESS
, which is a command it always gives during power on.
package nl.edubits.thuis.server.tv.cec; @Startup @ApplicationScoped public class CecObserverBean { @Inject MqttService mqttService; public void tvMessage(@Observes @CecSource(TV) Message message) { if (message.getDestination() != BROADCAST && message.getDestination() != RECORDER1) { return; } switch (message.getOperator()) { case STANDBY: mqttService.publishMessage("Thuis/device/living/homeTheater/tv", "off"); break; case REPORT_PHYSICAL_ADDRESS: mqttService.publishMessage("Thuis/device/living/homeTheater/tv", "on"); break; case VENDOR_COMMAND_WITH_ID: if (message.getRawMessage().equals("0f:a0:08:00:46:00:09:00:01") || message.getRawMessage().equals("0f:87:08:00:46")) { mqttService.publishMessage("Thuis/device/living/homeTheater/tv", "off"); } default: break; } } }
The opposite happens in the MqttObserverBean
, which listens to MQTT messages and executes the corresponding CEC commands. Here we'll turn the TV on and off and then ask the TV to report its power status back:
package nl.edubits.thuis.server.tv.mqtt; @ApplicationScoped public class MqttObserverBean { @Inject private CecConnection connection; public void onActionMessageTV(@Observes @MqttTopic("Thuis/device/living/homeTheater/tv/set") MqttMessage message) { switch(message.asText()) { case "on": connection.sendMessage(new Message(RECORDER1, TV, IMAGE_VIEW_ON, Collections.emptyList(), "")); case "off": connection.sendMessage(new Message(RECORDER1, TV, STANDBY, Collections.emptyList(), "")); } connection.sendMessage(new Message(RECORDER1, TV, REPORT_POWER_STATUS, Collections.emptyList(), "")); } }
This concludes our implementation of the TV node. It's now able to listen to other CEC-enabled devices, communicate with them and bridge this through MQTT messages. In part 2 we'll take these MQTT messages, wrap them and create some scenes to turn everything on with a single button!