Multiroom Audio Out of IKEA's Ten-Euro Speaker
Four KALLSUP cubes, one Bluetooth dongle, a container on the home server and Home Assistant. Announcements in every room, radio where you want it, and a physical limit that shaped every decision.
Multiroom Audio Out of IKEA's Ten-Euro Speaker
IKEA sells a Bluetooth speaker called KALLSUP for about ten euros. It is
a small cube, it sounds fine for what it is, and the moment I saw it I
knew I wanted four of them — one per room — for two jobs: spoken
announcements from Home Assistant everywhere at once, and radio in
whichever room I happened to be in.
Forty euros of speakers, one USB Bluetooth dongle, and what turned into
one of the more educational projects I have done. Not because the
software was hard. Because the radio was.
The limit that decides everything
Bluetooth audio over A2DP with the standard SBC codec costs roughly
345 kbit/s per stream. A single dongle has to fit every stream into the
same 2.4 GHz slice, alongside the WiFi and everything else in the flat.
The Realtek chip in a typical dongle handles two, maybe three of those
streams reliably. Four is possible only at reduced bitrate and only in a
quiet radio environment. Nothing in software changes this. PipeWire,
PulseAudio, Snapcast, BlueALSA — every stack produces one A2DP stream per
speaker, and the airtime is the airtime.
I did not know this when I started. It took a while to accept that the
crackling was physics, not a bug. Once I did, the design fell out almost
on its own.
Two modes, not one
If four high-quality streams cannot coexist, do not try. Instead:
Announcement mode — all four speakers connected, low SBC bitrate.
Speech does not need more. This is the default state.
Music mode — one room, exclusive. Everything else disconnects, the
one remaining speaker gets the whole channel. Radio sounds like radio.
Switching between them is one HTTP call. The rest of the system is built
to make both modes robust, because Bluetooth speakers are not designed to
be permanently attached to a Linux box and behave accordingly.
The chain
Five pieces. Each earns its place.
1. The Bluetooth container
A privileged container on the host, with its own dbus-daemon and
bluetoothd so it does not fight the host's Bluetooth stack. On top of
that, BlueALSA as the A2DP source:
BlueALSA rather than PulseAudio or PipeWire because it is the leanest
thing that does headless A2DP source in a container, and because the
projects that tried PulseAudio for exactly this use case moved away from
it for stability reasons.
For each speaker there is a supervise loop: once the device reports
Connected: yes, set the mixer to full and start a squeezelite
instance pointed at that BlueALSA device, with a fake MAC so Music
Assistant sees four distinct players. If squeezelite dies — speaker
switched off, walked out of range — retry every fifteen seconds.
2. The watchdog, and why it escalates
Every sixty seconds, reconnect anything missing. In music mode, only the
one room that is supposed to be there.
Then the part that matters: if no speaker is reachable five times in a
row, the problem is almost certainly not the speakers. The Realtek
firmware has hung. That is a known, still-open kernel issue in 2026, and
the only fix is a USB reset of the adapter. The watchdog stops
bluetoothd and bluealsa cleanly, resets the USB device, and exits the
container so Docker restarts it fresh. Rate-limited to once every thirty
minutes so a genuinely broken dongle cannot reset itself in a loop.
Without this, the setup worked until it silently did not, and someone
would notice hours later that the kitchen had gone quiet.
3. The mode service
Home Assistant runs in a VM. The Bluetooth container runs on the host.
They cannot talk directly, so a tiny HTTP service on the host sits in
between and knows exactly two verbs:
- mode=music&room=X — disconnect the others, mark X as exclusive,
restart BlueALSA, connect X
- mode=normal — clear exclusive, restart BlueALSA, connect all four
No authentication. It lives on the LAN, it can do two things, and both of
them are undone by the other. That is an acceptable trade for something
this small.
4. Music Assistant
Music Assistant handles the actual playback. The four squeezelite players
appear through its squeezelite provider, wrapped so they show up in Home
Assistant as normal mediaplayer entities.
Two per-player settings do most of the work: a short chime before every
announcement, and volume normalisation so a TTS voice and a radio stream
do not arrive at wildly different levels.
Music Assistant's announcement feature also solves a problem I had
expected to write code for: if music is playing in a room, an
announcement pauses it, speaks, and resumes. Free.
5. Home Assistant scripts
One script is the only entry point for announcements. It takes a target
and text, picks a volume by time of day — quieter at night, full during
the day — renders the speech with Piper, runs it through an ffmpeg EQ
chain so it does not sound like a phone speaker in a tin, and hands it to
Music Assistant with the announce flag set.
Radio toggles are per room: if something is playing, stop it and drop
back to announcement mode; otherwise switch to music mode, wait for the
player to come up, set a sensible volume and start the stream.
Around that, a handful of triggers: a wake-up automation that starts
radio in the bedroom ten minutes before the phone alarm and returns to
announcement mode afterwards, two cheap IKEA buttons, a voice satellite
whose button and dial control the living room. And a push notification if
any room has been unavailable for more than ten minutes, which is how I
find out about dead batteries.
What still is not solved
Phones steal speakers. A KALLSUP will connect to whichever known
device asks. If a phone has paired with the bedroom cube, the cube may
simply leave. There is no whitelist and no multipoint on a ten-euro
speaker. The only real mitigation is on the phones — forget the device,
or turn off auto-connect. The other trick, which I have not yet
implemented: in music mode, keep the three unused speakers connected but
idle instead of disconnecting them. A connected single-source sink
cannot be stolen, and an idle connection costs almost no airtime.
No true sync. Four independent A2DP streams drift. For speech it is
irrelevant. For music across rooms it would not be acceptable, which is
one more reason music mode is one room.
Music is one room only. By design, see above.
Is this actually the best way
Having built it, I spent an afternoon seriously checking whether a
cleaner solution existed — with the constraint that the hardware stays
exactly as it is. PipeWire with combine-stream, PulseAudio with
combine-sink, Snapcast, Home Assistant add-ons with USB passthrough into
the VM, better codecs, different players.
Almost all of them fail on the same point: they do not change the number
of A2DP streams on the dongle, so they cannot fix the thing that actually
hurts. Higher-quality codecs make it worse by raising the bitrate.
Passthrough into the VM would remove the host-level USB reset, which is
the single most important piece of the watchdog.
One project is worth a parallel test: a recently-active Bluetooth bridge
for Music Assistant's Sendspin protocol, which would replace squeezelite,
the mode service and most of my scripting with a maintained tool that has
a web UI and native announcements. It is young and PulseAudio-based, so
I am not switching until it runs four speakers at least as stably as the
current setup. If it does not, the homebuilt version is objectively the
better one, which is a strange and satisfying thing to be able to say
about a bash script.
Was it worth it
Forty euros of speakers, a fifteen-euro dongle, and hardware I already
owned. In return: anyone in the kitchen can say something to the whole
flat, the bedroom wakes up to radio, and I understand Bluetooth audio
far better than I ever wanted to.
The lesson I keep coming back to: find the physical limit first. Every
hour I spent before accepting the two-to-three-stream reality was spent
fixing the wrong thing. Every design decision after that was easy.