When you say "Home Automation" to many people they picture some kind of futuristic Iron-Man-esque fully-automatic robot home, but often, the best things are really very small. Tiny optimisations that make things just a little bit nicer - like my "Family Helper" that remembers things for us. It's not for everyone, and it's not going to change the world, but it's been good for us.
In that vein, here's another little optimisation that streamlines out a little annoyance we've had since getting a Google Chromecast Ultra. We love being able to ask the Google Home to play something on Spotify, and with the Chromecast plugged directly into the back of my Yamaha AV receiver via HDMI, it sounds fantastic too. There's just one snag, and fixing it means walking over to the AV receiver and changing the input to HDMI2 ("Chromecast") manually, which (#firstworldproblems) kinda undoes the pleasure of being able to use voice commands.
It comes down to the HDMI CEC protocol, which is how the AV receiver is able to turn on the TV, and how the Chromecast turns on the AV receiver. It's cool, handy, and most of the time it works well. However, when all the involved devices are in standby/idle mode, and a voice command to play music on Spotify is issued, here's what seems to be happening:
Time |
Chromecast |
AV receiver |
Television |
0 | OFF | OFF | OFF |
1 | Woken via network | | |
2 | Sends CEC "ON" to AVR | | |
3 | | Wakes | |
4 | | Switches to HDMI2 | |
5 | AV stream starts | | |
6 | | Detects video | |
7 | | Sends CEC "ON" to TV | |
8 | | | Wakes |
9 | | Routes video to TV | |
10 | | | "Burps" via analog audio out |
11 | | Hears the burp on AV4 | |
12 | | Switches to AV4 | |
Yes, my TV (a Sony Bravia from 2009) does NOT have HDMI ARC (Audio Return Channel) which may or may not address this. However, I'm totally happy with this TV (not-"smart" TVs actually seem superior to so-called "smart" TVs in many ways).
The net effect is you get a few seconds of music from the Chromecast, before the accompanying video (i.e. the album art image that the Chromecast Spotify app displays) causes the TV to wake up, which makes the amp change to it, which then silences the music. It's extremely annoying, especially when a small child has requested a song, and they have to semi-randomly twiddle the amp's INPUT knob until they get back to the Chromecast input.
But, using the power of the Chromecast and Yamaha Receiver OpenHAB bindings, and OpenHAB's scripting and transformation abilities, I've been able to "fix" this little issue, such that there is less than a second of interrupted sound in the above scenario.
The approach
The basic approach to solve this issue is:
- When the Chromecast switches to the Spotify app
- Start polling (every second) the Yamaha amp
- If the amp input changes from HDMI2, force it back
- Once 30s has elapsed or the input has been forced back, stop polling
Easy right? Of course, there are some smaller issues along the way that need to be solved, namely:
- The Yamaha amp already has a polling frequency (10 minutes) which should be restored
- There's no way to (easily) change the polling frequency
The solution
Transformation
First of all, we need to write a JavaScript transform function, because in order to change the Yamaha polling frequency, we'll need to download the Item's configuration as JSON, alter it, then upload it back into the Item:
transform/replaceRefreshInterval.js
(function(newRefreshValuePipeJsonString) {
var logger = Java.type("org.slf4j.LoggerFactory").getLogger("rri");
logger.warn("JS got " + newRefreshValuePipeJsonString);
var parts = newRefreshValuePipeJsonString.split('|');
logger.warn("JS parts: " + parts.length);
var newRefreshInterval = parts[0];
logger.warn("JS new refresh interval: " + newRefreshInterval);
var entireJsonString = parts[1];
logger.warn("JS JSON: " + entireJsonString);
var entireThing = JSON.parse(entireJsonString);
var config = entireThing.configuration;
logger.warn("JS config:" + JSON.stringify(config, null, 2));
// Remove the huge and noisy album art thing:
config.albumUrl = "";
config.refreshInterval = newRefreshInterval;
logger.warn("JS modded config:" + JSON.stringify(config, null, 2));
return JSON.stringify(config);
})(input)
Apologies for the verbose logging, but this is a tricky thing to debug. The signature of an OpenHAB JS transform is effectively
(string) => string so if you need to get multiple arguments in there, you've got to come up with a string encoding scheme - I've gone with pipe-separation, and more than half of the function is thus spent extracting the args back out again!
Basically this function takes in
[new refresh interval in seconds]|[existing Yamaha item config JSON], does the replacement of the necessary field, and returns the new config JSON, ready to be uploaded back to OpenHAB.
Logic
Some preconditions:
- A Chromecast Thing is set up in OpenHAB
- With #appName channel configured as item LivingRoomTV_App
- A Yamaha AVReceiver Thing is set up in OpenHAB
- With (main zone) #power channel configured as item Yamaha_Power
and
- (Main zone) #input channel configured as item Yamaha_Input
rules/chromecast.rules
val AMP_THING_TYPE="yamahareceiver:yamahaAV"
val AMP_ID="5f9ec1b3_ed59_1900_4530_00a0dea54f93"
val AMP_THING_ID= AMP_THING_TYPE + ":" + AMP_ID
val AMP_URL = "http://localhost:8080/rest/things/" + AMP_THING_ID
var Timer yamahaWatchTimer = null
rule "Ensure AVR is on HDMI2 when Chromecast starts playing music"
when
Item LivingRoomTV_App changed
then
logInfo("RULE.CCAST", "Chromecast app is: " + LivingRoomTV_App.state)
if(yamahaWatchTimer !== null) {
logInfo("RULE.CCAST", "Yamaha is already being watched - ignoring")
return;
}
if (LivingRoomTV_App.state == "Spotify") {
logInfo("RULE.CCAST", "Forcing Yamaha to power on")
Yamaha_Power.sendCommand("ON")
// Fetch the Yamaha thing's configuration:
var yamahaThingJson = sendHttpGetRequest(AMP_URL)
logInfo("RULE.CCAST", "Existing config is: " + yamahaThingJson)
// Replace the refresh interval field with 1 second:
var newYamahaConfig = transform(
"JS",
"replaceRefreshInterval.js",
"1|" + yamahaThingJson
)
logInfo("RULE.CCAST", "New config is: " + newYamahaConfig)
// PUT it back using things/config:
sendHttpPutRequest(
AMP_URL + "/config",
"application/json",
newYamahaConfig.toString())
logInfo("RULE.CCAST", "Forcing Yamaha to HDMI2")
Yamaha_Input.sendCommand("HDMI2")
logInfo("RULE.CCAST", "Forced Yamaha to HDMI2")
logInfo("RULE.CCAST", "Will now watch the Yamaha for the next 30")
logInfo("RULE.CCAST", "sec & force it back to HDMI2 if it wavers")
val DateTimeType ceasePollingTime = now.plusMillis(30000)
yamahaWatchTimer = createTimer(now, [ |
if(now < ceasePollingTime){
Yamaha_Input.sendCommand("REFRESH")
logInfo("RULE.CCAST", "Yamaha input: " + Yamaha_Input.state)
if (Yamaha_Input.state.toString() != "HDMI2") {
logInfo("RULE.CCAST", "Force PUSH")
Yamaha_Input.sendCommand("HDMI2")
}
yamahaWatchTimer.reschedule(now.plusMillis(1000))
}
else {
logInfo("RULE.CCAST", "Polling time has expired.")
logInfo("RULE.CCAST", "Will not self-schedule again.")
var revertedYamahaConfig = transform(
"JS", "replaceRefreshInterval.js",
"600|" + yamahaThingJson
)
sendHttpPutRequest(
AMP_URL + "/config",
"application/json",
revertedYamahaConfig.toString()
)
logInfo("RULE.CCAST", "Yamaha polling reverted to 10 minutes.")
yamahaWatchTimer = null
}
])
}
end
Some things to note. This uses the "self-triggering-timer" pattern outlined in the OpenHAB community forums, reads the configuration of a Thing using the REST interface as described here, and is written in the XTend dialect which is documented here.