What My Nest Hub Does Besides Show Family Photos
There's a Google Nest Hub sitting on a shelf under the living room TV, Google hasn't killed off this device (yet) so lets make it do more than show photos. Out of the box it's a photo frame with a clock on it, which is fine, but a screen that's already sitting there, already networked, already close to the action, felt like it should be doing more. It lives there muted, isn't really used for smart home stuff, it could be with one of the Home Assistant apps (docker containers) but I haven't explored that too much. I've seen plenty of home theater builds where people wire up a display that shows what's currently playing , the title, the poster art, how much longer it runs. I don't have a home theater. I have a living room. But I liked the idea anyway, mostly for one specific reason: when I start something on the Apple TV, I want to glance over and see when it's going to be done. If I pause it and start it back up later, I want that same glance to show me fresh info, not what was true an hour ago. The other thing I wanted from that same shelf display had nothing to do with video at all. Most of the cameras around the house already show up as HomeKit notifications in the corner of the Apple TV screen, so those are covered. The driveway PTZ camera is the odd one out. It catches a lot of things that aren't really notification-worthy, someone walking past on the sidewalk, the neighbor's cat cutting through the yard, kids messing around near the roses out front, so it's never wired into HomeKit alerts. I still wanted an easy way to glance at it without grabbing a phone, just not as an interruption on the TV itself. Also, our TV isn't on 24/7, we watch non linear tv so we don't have the news just running non stop or a tv show we dont care about. With the TV off you could glance at this screen on the shelf and see whats going on. Perfect. Two jobs, one screen, and neither one has anything to do with family photos or what this device was even designed for, I love stuff like this. When someone or something shows up on the driveway camera, the shelf display should switch over to a low-res feed for a minute or two, then go back to normal on its own. No notification, no phone, just a glance if I happen to be in the room. When something starts playing on the living room Apple TV, whether that's a movie through Moonfin, a show, or a YouTube video, the same display should flash a card: the title, the channel or show it belongs to, and a projected finish time. If I pause and resume, it should refresh with an updated time, not just repeat the same stale card from when I first hit play. Home Assistant already had entities for both halves of this. A media_player tracking the Apple TV over AirPlay, and a media_player for the Nest Hub through the Cast integration. On paper, wiring the driveway camera side up was trivial, one action that plays a camera stream to a cast target. The now-playing card looked just as simple. It wasn't. The driveway camera automation worked on the first try: The now-playing card did not. My first attempt looked just as reasonable, a media_player.play_media call pointed at a generated thumbnail: I wired that into an automation that triggered on the Apple TV's media_title changing, pulled the artwork, and cast it with a title and subtitle overlay. Home Assistant reported success every single time. The media player's state cycled through idle, buffering, playing. The title updated. Everything about the API said it worked. The Nest Hub's screen didn't change. At all. The obvious suspect was the image itself, maybe the thumbnail was broken or the fetch was failing silently. I checked. It wasn't. Next I suspected the Cast integration's URL validation, which turned out to be real but unrelated. Home Assistant's Cast component refuses to cast a media_content_id that resolves to its own host, whether that's a cloud URL or a plain LAN address: That fix is real and worth keeping on its own merits. Reference local files through media-source://media_source/local/ instead of building a URL by hand, and Home Assistant resolves it into a signed, cast-safe URL automatically. That killed the error message. It did not put anything on the screen. I kept narrowing it down. Real Jellyfin artwork over a plain HTTP URL, nothing. A photoMediaMetadata type instead of the generic one, nothing. The simplest possible test, a public image URL with zero moving parts: Home Assistant logged a clean success again. The Nest Hub sat there showing its ambient screensaver like nothing had happened. At that point there was only one conclusion left. This specific device does not render generic image casts, on this Home Assistant version, full stop. Every layer I'd been debugging, metadata types, signed URLs, content types, was real, but none of it was the actual problem. The driveway camera automation had been working the entire time, on the same Nest Hub, through the same Cast integration. I'd been treating it as a separate feature instead of a clue, because casting a camera stream feels conceptually different from casting a photo. It isn't, not to the device. A camera stream is video. That's the whole difference. This Nest Hub renders video casts fine and just does nothing visible with a plain image, no matter how it's addressed or labeled. Somewhere in the Cast "Default Media Receiver" on this device, static images and video streams take separate code paths, and only one of them is wired up to actually paint pixels. The fix, once I stopped fighting it, was almost funny. Stop casting an image, and cast a 30 second silent video of the image instead. I think 30 seconds is plenty of time to see whats playing, what the time it will be done is. ffmpeg was already sitting on the Home Assistant host, bundled in for camera and streaming support. That meant the whole now-playing card could be one generated clip: thumbnail on the left, channel name and title on the right, and a big, clearly labeled projected finish time pinned to the bottom. Cast that as video/mp4, and the Nest Hub showed it immediately. media_duration finally reported a real number instead of 0. This device I suppose only likes video casted to it, so we did just that. The text gets written to files first, then read back with drawtext's textfile option, instead of sitting inline in the filter graph. Real titles contain colons, quotes, and apostrophes that would otherwise collide with ffmpeg's own filter syntax. That one decision avoided a whole second debugging session. The thumbnail-left, text-right layout worked fine for YouTube, where the source thumbnail is already roughly square. It looked wrong for anything coming through Moonfin, though. Movie and show posters are portrait, and cropping one down into a small square just showed an oddly zoomed sliver of the art. Jellyfin and Moonfin content gets a different treatment now: a full-bleed backdrop image filling the whole frame, with the movie or show's logo graphic centered near the bottom over a dark scrim. When an item doesn't have a logo in Jellyfin, which happens more than you'd expect, the title gets drawn as bold text in that same spot instead. Picking the right artwork took its own fallback chain. Episodes rarely carry their own backdrop or logo, so the automation walks up to the season, then the series, until it finds one. Movies have neither a parent nor a series to fall back to, so they just use their own artwork directly. Deciding whether an item qualifies for this treatment at all turned into the hardest bug of the whole project. Even once the hero layout worked, it only showed up sometimes. The automation would flip between the backdrop treatment and the plain classic layout across nearly identical Jellyfin sessions, with no obvious pattern and nothing useful in the logs. This is the kind of bug that eats a weekend for a reason that has nothing to do with ffmpeg or Home Assistant at all. The automation decides which layout to use with one boolean: An earlier version of that line just chained the three checks together with a plain "and." That seemed fine, but "and" in this language doesn't actually hand back true or false, it hands back whichever value made the decision. In this case, that was a Jellyfin ID, not a clean yes or no. Most of the time that didn't matter, since the ID still counted as "something" wherever it showed up. But one specific check in Home Assistant is pickier about what counts as true, and an ID string doesn't qualify. So the same value passed one check and quietly failed another a few lines later. Forcing it into a real true or false fixed it for good. That's what "sometimes works, sometimes doesn't" really was: one value being read two different ways by two parts of the automation that never agreed on what counted as true. shell_command doesn't run through a real shell. I assumed Home Assistant's shell_command integration passed commands through /bin/sh, the way you'd expect. It doesn't. For templated commands it execs the rendered, split argument list directly. A plain redirect using the greater-than sign silently did nothing, no error, no file, no clue. I stopped relying on shell syntax entirely and wrote text out from a oneliner Python instead: A family member was watch a show on Jellyfin at their house and it threw this code off a bit. It was reporting the show they were watching not the show I was watching and their AppleTV was named "Apple TV" as well. Two Jellyfin clients were both named "Apple TV" and both running Moonfin for tvOS, one on the LAN, one connecting in over Tailscale from somewhere else. The only field that told them apart was ipAddress. Pinning the lookup to this Apple TV's reserved address fixed it for good: Once there were three visual layouts to choose between, I ran into a wall: these shell command templates don't support if/else logic, just simple substitution. So instead of one command that branches internally, there are three separate commands now, one per layout, and the automation picks which one to call. ffmpeg also choked when I fed it two images at once for the hero layout, a backdrop and a logo composited together. It would just hang. The fix was to flatten the images into a single frame first, then loop that one frame into the finished clip as a separate step. A fresh video starting actually trips two triggers at once. Without a small delay on both, one would fire and finish before the other had even started, so the second run would cast right on top of the first. Giving both triggers the same five-second wait fixed it. And a couple smaller ones: filenames need to be unique per run, since a superseded automation run keeps writing files in the background even after being canceled, and those files could clobber a newer run's output. Tagging every filename with the run's own ID took care of it. Both automations target the same media_player, so they need to stay out of each other's way. The now-playing automation checks that nobody's currently at the driveway before it takes over the display, so a movie card can't interrupt a driveway alert that's already showing: The driveway automation is the simpler of the two, since a camera stream is already video and never ran into the image problem at all: The now-playing automation is longer, mostly because it tries to figure out a good title, channel, and thumbnail no matter what's playing, Jellyfin through Moonfin, a YouTube video, or anything else the Apple TV reports. The load bearing part is really just three steps buried inside it: write the channel, title, and finish time out to text files, composite them into a video with ffmpeg, then cast that video with play_media. Everything else, the Jellyfin session lookup, the YouTube thumbnail fallback, is optional flourish on top of that core trick. A few notes if you're adapting either of these. The IP addresses and device ID are specific to this household and need to change for anyone reusing them. The two DejaVuSans font paths point at fonts bundled with other, unrelated HACS integrations, since there's no dedicated system font directory on this Home Assistant OS install. It works, but it's not something I'd call the right way to do it, just what was already sitting on the disk. Start something through Moonfin and the shelf display switches to the full backdrop treatment. When the movie or show has a logo, it sits centered near the bottom over the artwork. Without one, the title takes its place instead. YouTube and anything else keeps the original layout, thumbnail on the left, channel and title on the right. Either way, the same "Ends @ 1:47PM" line sits at the bottom, and it's gone again after about thirty seconds, cleaning up its own temp files as it goes. Pause the video and start it again later, and the same trigger fires again with a fresh finish time, since the automation reads the Apple TV's current position and duration each time rather than remembering the first calculation. Walk up the driveway while something's playing, and the driveway feed wins instead, then hands the screen back once things are quiet. None of the individual pieces here are exotic. ffmpeg's drawtext filter, media-source:// URIs, a REST session lookup, are all fairly ordinary Home Assistant tools. What made the difference was realizing this device already had a working video cast sitting right next to the broken image cast, and that the two behaving differently was the whole answer. A photo frame turned out to be a fine screen for showing more than photos, once I stopped asking it to show a photo. We sometimes build things just to see if we can, sometimes we build things because its fun. Sure some of this is redundant such as the end time for a Jellyfin video but YouTube doesnt display this so why not. Have you built anything like this? I'd love to hear it in the comments below! Make a quick free acocunt here and leave a comment. It keeps the spammers out and lets me know people care about my adventures in technology.