Skip to main content

Bringing Disney Magic to Your Garden: The Solar-Powered Birdbox Music Player

·1821 words·9 mins

Introduction
#

There’s a particular sound that Disney parks are built around, and it isn’t any one song. It’s the layer underneath everything else — the loops of Main Street brass, the wind-chime shimmer of Adventureland, the low choral hum outside the Haunted Mansion — that a park plays constantly and almost nobody consciously notices until it stops. It’s ambience as architecture. I wanted a piece of that in my own garden, on demand, without dragging a phone speaker outside every time.

The idea was simple to say and, it turned out, less simple to build reliably: a weatherproof outdoor box that looks like an ordinary birdhouse, runs off solar and battery power, and quietly plays Disney park ambience and attraction music through a small speaker — controllable from Home Assistant, and smart enough to react to things happening in the real world. Coming home in the evening. Halloween season. A Disneyland ride’s live wait time dropping low enough that, if I were actually there right now, I’d probably be walking toward it.

What follows is the build: the hardware, the software, the two or three bugs that ate an entire evening each, and where the project actually stands today, which is not quite finished — more on that in a moment.

Design & Hardware
#

The brain of the thing is an ESP32-WROOM-32, running ESPHome rather than raw Arduino code — mainly because ESPHome hands you Home Assistant integration, OTA firmware updates, and a local web diagnostics server almost for free, and this project was always going to live or die on how well it talked to Home Assistant. Audio playback is handled by a DFPlayer Mini, a tiny and extremely cheap MP3 module that takes serial commands over UART and drives a small speaker directly — in this case a 4-ohm, 5W unit around 40-50mm across, plenty for garden-ambience volume without needing an amplifier stage.

Power is the part that makes this a garden project rather than a desk project. Two Samsung 35E 18650 cells sit in parallel in a battery holder, giving roughly 7000mAh at 3.7V, charged through a USB-C Li-ion charger module with its own protection circuit. A small 5-6V, 1-2W solar panel feeds into the same charger’s 5V input. Power draw during active playback sits around 500mA at medium volume, dropping to roughly 30mA in deep sleep (the ESP32 and DFPlayer idling at about 10mA and 20mA respectively) — which works out to something like a week of battery life on typical use (around 30 minutes of playback a day) even with no sun at all, and effectively indefinite runtime once the solar panel is getting 5+ hours of daylight.

The wiring itself is deliberately simple: battery through the USB-C charger to both the ESP32’s VIN and the DFPlayer’s VCC, a shared ground across all three components, ESP32 GPIO17 to the DFPlayer’s RX and GPIO16 to its TX for UART, and the DFPlayer’s speaker terminals straight to the speaker. Everything sized to fit inside a birdbox enclosure — which is doing double duty here as both the housing and the disguise. A speaker grille that looks like ventilation, a solar panel that looks like a roof panel, and from six feet away it’s just a birdbox that happens to be having an unusually good day.

Software & Playlist Curation
#

This is the section where the project actually fought back.

The original plan was tidy: organize all 65 tracks — pulled from Disneyland, Disney California Adventure, Epcot, Hollywood Studios, and Disneyland Paris — into numbered folders on the SD card (01, 02, 03…), and use the DFPlayer’s play_folder(folder, track) command to jump straight to any specific track. This is the textbook way to structure a DFPlayer library, and it’s what every tutorial shows.

It didn’t work. Direct track-number selection inside a folder consistently failed with “specified track is not found,” even after triple-checking the SD card was properly FAT32-formatted and that the alphabetical file order matched what the DFPlayer expected. The confusing part was that some things worked fine — “play all in folder” ran correctly, and “next track” advanced properly. Only the specific folder-plus-track-number addressing mode failed. That inconsistency was the clue: this wasn’t a filesystem problem, it was a cheap DFPlayer Mini clone that simply doesn’t implement the two-argument play_folder command reliably, despite accepting it without error.

The fix ended up being a full restructure: every track got dumped into a single flat folder named MP3 at the SD card’s root, renamed sequentially with four-digit numbers — 0001.mp3 through 0065.mp3 — using a small batch script to automate the renaming in one pass. Track selection then goes through the DFPlayer’s simpler play_mp3(number) command instead, addressing files by absolute position rather than folder-plus-index. It has been completely reliable ever since. If you’re building anything on a DFPlayer Mini clone, this is worth knowing before you spend an evening staring at the exact same error I did.

With playback sorted, the ESPHome firmware (birdbox.yaml) exposes the whole thing as ordinary Home Assistant entities: a volume number control, a track-number control, text sensors for what’s currently playing, a “Random Mode” switch that auto-continues with a random track when the current one ends, and a full set of transport buttons — play, pause/resume, stop, next, previous, volume up/down, and a remote restart. Two smaller lessons live in that firmware file too: ESPHome won’t accept a / character in entity names (it reserves the character for URL paths, so “Pause/Resume” had to become “Pause or Resume”), and the older .state property on select components is deprecated in favor of .current_option().

On the Home Assistant side, a master script — birdbox_play_track_by_name — takes a filename, looks it up in an internal map against the 65 track numbers, and handles setting the track and pressing play. Every automation calls this one script rather than juggling raw track numbers directly, which turned out to matter later: when Home Assistant needs to react to something in the real world, it should be reasoning in filenames (“Haunted Mansion score”), not remembering that track 34 happens to mean that this week.

And react it does. A Halloween automation checks whether it’s October, and if so, plays one of three Haunted Mansion or “Le Bat en Rouge” tracks at 6pm daily, chosen at random via a Jinja random filter — which surfaced a genuinely subtle YAML lesson: writing that template with the literal block scalar (|) passes the text through as-is instead of evaluating it, silently breaking the whole automation with no error, whereas the folded block style (>) lets it evaluate correctly. A “welcome home” automation plays Main Street background music when the front door opens between 5pm and 10pm. An “away from home” automation stops playback five minutes after everyone leaves. And the one I like best: a live automation watching 19 different Disneyland and California Adventure ride wait-time sensors, and when any of them drops below 15 minutes during evening hours while I’m actually home, it plays that specific ride’s themed track — Big Thunder Mountain, Space Mountain, Pirates of the Caribbean, Rise of the Resistance, and so on, each mapped to its own file. It’s a small thing, but there’s something genuinely fun about a birdbox reacting to real park conditions three thousand miles away.

Assembly & Installation
#

Here’s the honest status update: everything above — hardware wiring, firmware, the Home Assistant integration, the automations, the dashboard — is built, debugged, and working, verified on the bench with the ESP32 and DFPlayer wired up outside the enclosure. What’s still ahead is the physical build-out: mounting all of it inside the actual birdbox housing, integrating the solar panel and battery pack in situ, weatherproofing the enclosure against actual garden conditions, and finally getting it mounted outside.

That’s a deliberate order of operations, not a delay. Getting the play_folder bug sorted, rebuilding the SD card structure, and rewriting the scripts and dashboard around the fix that actually worked all needed to happen with the electronics easily accessible on a bench, not sealed inside a weatherproofed box in the yard. Now that the software side is stable, the remaining steps are the more mechanical ones: securing the ESP32, DFPlayer, and battery holder inside the housing, routing the speaker to sit behind a grille that still looks convincingly like a birdbox, mounting the solar panel where it’ll actually see sun, and sealing every seam against rain before it goes anywhere near the garden. I’ll follow up with the physical build once that part’s done — including whatever I inevitably get wrong about weatherproofing a project that’s spent its entire life so far indoors.

The Lovelace dashboard, at least, is fully finished and already usable — a “Now Playing” header, transport controls, a Quick Play grid for favorite tracks (Main Street, Star Trader, Orleans Square at night, the Haunted Mansion score, Soarin’, Space Mountain), and a “Play by Collection” button grid, one tap per park. That last one exists because of a smaller lesson worth passing on: Home Assistant’s Lovelace entity-row buttons don’t reliably support double-tap actions for triggering two dependent service calls in sequence. The original collection selector needed exactly that, silently failed, and got replaced with a dedicated script per collection plus a single-tap button grid — more buttons, but every one of them actually works.

Future Enhancements
#

Some of what’s next is already on the list: LED status indicators, physical manual control buttons on the enclosure itself for when reaching for a phone feels like overkill, battery-level reporting back into Home Assistant so the dashboard can warn before it actually runs flat, deep-sleep power tuning for longer battery-only stretches when the solar panel isn’t pulling its weight, seasonal scheduling profiles beyond just the Halloween automation, and playlist support with proper fade-in/fade-out transitions between tracks instead of the current hard cut. I did seriously consider routing this through Music Assistant instead of the DFPlayer/SD-card setup, and concluded the opposite of what I expected going in — for a single outdoor speaker with a fixed library, the simpler local-file architecture is more reliable than adding a networked media player into the chain. Sometimes the boring answer is the right one.

Beyond the documented list, voice control is the obvious next reach — “Hey Google, play Main Street” is a more natural interface than opening a dashboard, and Home Assistant’s voice pipeline is already sitting right there. Multi-zone is the other one worth dreaming about: a second unit somewhere else in the garden, synced or offset, so the ambience actually moves with you the way it does walking between lands in a real park. Neither of those has a design yet — they’re the kind of idea that shows up on a list like this specifically so I remember I had it, six months from now when the birdbox is finally mounted and I’m looking for the next reason to open the enclosure back up.