Broan/NuTone ChromaComfort - Control + Audio

I have a Broan/NuTone ChromaComfort/Sensonic bathroom fan with the integrated Bluetooth speaker, white light, and RGB light.

I’ve wanted to get it properly integrated into Home Assistant for a while, but there’s an annoying limitation: the fan expects a Bluetooth connection for control, and tying up that connection with a controller can interfere with actually using the Bluetooth speaker.

So I went down the rabbit hole of figuring out the protocol and eventually built a Linux bridge that does both.

The end result is a small Linux box that maintains the Bluetooth connection to the ChromaComfort and exposes:

  • Fan on/off in Home Assistant
  • White light on/off + brightness
  • RGB light on/off + color + brightness
  • Wall RGB mode
  • Real-time-ish device status
  • MQTT/Home Assistant Discovery
  • Connection/packet/ACK diagnostics in HA
  • The built-in speaker as an AirPlay destination
  • Spotify/iPhone volume control through AirPlay

The interesting part is that the Linux host maintains both Bluetooth Classic RFCOMM/SPP for controlling the fan and A2DP for audio at the same time.

The architecture ended up looking like this:


                         ChromaComfort-Sensonic
                         /                    \
                RFCOMM / SPP                A2DP
                      /                        \
      chromacomfort_bridge.py               BlueZ
                 |                             |
                MQTT                    PipeWire/WirePlumber
                 |                             |
          Home Assistant                Shairport Sync
                                               |
                                            AirPlay
                                               |
                                         Spotify/iPhone

There were a few fun quirks along the way.

The control protocol requires retransmitting commands until the fan acknowledges them. RGB colors use the fan’s “favorite color” functionality. The periodic status packets expose brightness but don’t appear to expose the current RGB values, so the bridge remembers successfully commanded colors.

Linux Bluetooth was probably the most entertaining part. After reboot, the device could report Connected: yes because the RFCOMM connection was established while the A2DP audio sink didn’t actually exist. BlueZ would also occasionally come up in a state where bluetoothctl just hung.

So the project now includes boot-time health checks that verify the things we actually care about instead of trusting Bluetooth’s connected flag. If BlueZ is wedged it gets recovered, and if the PipeWire A2DP sink doesn’t appear, the audio connection is retried until it does.

It’s now survived reboot testing with both HA control and AirPlay audio coming back automatically.

I packaged everything up here:

For Debian/Ubuntu I’ve also put together an installer:

git clone https://github.com/Relkci/ChromaComfort-Python-Control.git
cd ChromaComfort-Python-Control
sudo bash scripts/install-linux.sh

It prompts for the ChromaComfort Bluetooth MAC, MQTT information, Home Assistant naming, and AirPlay speaker name.

Huge credit to Taylor Finnell’s previous ChromaComfort reverse-engineering work, which gave me the foundation for understanding the Bluetooth serial protocol.

I expanded on that while figuring out the status packets, HA/MQTT bridge, Linux RFCOMM behavior, brightness/color control, and simultaneous A2DP audio.

If anyone else has one of these fans, I’d be particularly interested in hearing whether different hardware revisions expose the same RFCOMM channel/protocol and whether the installer works cleanly on other Linux hardware.

Admittedly, this has been a ton of code & test. The diagnostic sensors are consequently growing, but they might help someone in the future who needs to troubleshoot any of the integrating components.

I’ve also added the ability to play notification’s direclty on the speaker via the bridge, so you’re not tied to just airplay.
The repo contains a couple very short wav’s for example.

A HA action like this will play the notification on the ChromaComfort speaker via the bluetooth audio bridge.

action:
  - action: mqtt.publish
    data:
      topic: chromacomfort/bathroom/audio/play
      payload: doorbell

add new sounds in /opt/chromacomfort/sounds/

If the AirPlay reciever is already playing music, the notification is just mixed in on top.

                                      ChromaComfort-Sensonic
                                      /                    \
                             RFCOMM / SPP                A2DP
                                   /                        \
                   chromacomfort_bridge.py               BlueZ
                              |                             |
                             MQTT                    PipeWire/WirePlumber
                              |                       /             \
                       Home Assistant        Shairport Sync      Alert WAVs
                                                  |                  |
                                               AirPlay          MQTT trigger
                                                  |                  |
                                            Spotify/iPhone     Home Assistant

I also added an update script that will update an existing install since I’ve made additional changes:

cd /opt/ChromaComfort-Python-Control
git pull
sudo bash scripts/update-linux.sh

Lastly, since there are so many race-conditions with the dependent services to make everything come up in the right order, I’ve added a diagnostic script that checks the status for things:

chromacomfort-status.sh

The handler will also report the current boot status in MQTT to be reported in HA.