This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

configs.gg23.audio: Bluetooth A2DP receiver into the hub

acheron gets BlueZ (adapter "gg23", UB500 dongle) as an A2DP sink only;
WirePlumber's bluez monitor targets received streams at the "hub" sink,
like roc-lan and AirPlay. A bt-agent (NoInputNoOutput) accepts pairing
without PIN. A Home Assistant button (MQTT discovery) opens a 120s
discoverable/pairable window; afterwards bonded devices are trusted so they
reconnect any time, and a binary_sensor shows the window state.

author
Hermes
committer
lassulus
date (Oct 16, 2026, 10:47 PM CEST) commit 49f39a5a parent d48ef870
+120 -1
+120 -1
configs/gg23/audio/hub.nix
··· 2 2 # virtual "hub" sink, whose monitor is linked to the ROC sinks of the rooms 3 3 # selected with `audio-room` or from Home Assistant (MQTT). Senders: any LAN 4 4 # machine via ROC (while `gg23-audio` from tools/gg23-audio runs), barnacle 5 - # via AirPlay 2 (shairport-sync + nqptp). Rooms run ./room.nix. 5 + # via AirPlay 2 (shairport-sync + nqptp), phones via Bluetooth A2DP (paired 6 + # during the window opened by the "Bluetooth pairing" button). Rooms run 7 + # ./room.nix. 6 8 { 7 9 self, 8 10 config, ··· 56 58 icon = "mdi:speaker-multiple"; 57 59 }; 58 60 }) choices; 61 + 62 + # seconds acheron stays discoverable and pairable after the button press 63 + pairingTimeout = 120; 64 + 65 + bluetoothDiscovery = [ 66 + { 67 + topic = "homeassistant/button/bluetooth_pairing/config"; 68 + config = { 69 + name = "Bluetooth pairing"; 70 + unique_id = "acheron_bluetooth_pairing"; 71 + command_topic = "gg23/bluetooth-pairing/set"; 72 + icon = "mdi:bluetooth-connect"; 73 + }; 74 + } 75 + { 76 + topic = "homeassistant/binary_sensor/bluetooth_pairing/config"; 77 + config = { 78 + name = "Bluetooth pairing"; 79 + unique_id = "acheron_bluetooth_pairing_state"; 80 + state_topic = "gg23/bluetooth-pairing/state"; 81 + icon = "mdi:bluetooth-connect"; 82 + }; 83 + } 84 + ]; 59 85 in 60 86 { 61 87 services.pipewire = { ··· 111 137 "sink.props"."node.description" = "${r} (ROC)"; 112 138 }; 113 139 }) rooms; 140 + }; 141 + # phones play into the hub: acheron is only an A2DP sink (no headset or 142 + # LE audio roles), and the received stream targets "hub" 143 + wireplumber.extraConfig."10-bluetooth-hub" = { 144 + "monitor.bluez.properties"."bluez5.roles" = [ "a2dp_sink" ]; 145 + "monitor.bluez.rules" = [ 146 + { 147 + matches = [ { "node.name" = "~bluez_input.*"; } ]; 148 + actions.update-props."target.object" = "hub"; 149 + } 150 + ]; 151 + }; 152 + }; 153 + 154 + # The UB500 dongle (RTL8761BU) firmware comes from linux-firmware. 155 + # Discoverable and pairable only after the "Bluetooth pairing" button. 156 + hardware.bluetooth = { 157 + enable = true; 158 + settings.General = { 159 + Name = "gg23"; 160 + # audio/video major class, loudspeaker, so phones show a speaker 161 + Class = "0x200414"; 162 + DiscoverableTimeout = pairingTimeout; 163 + PairableTimeout = pairingTimeout; 164 + # a phone that forgot acheron can pair again during the window 165 + JustWorksRepairing = "always"; 166 + }; 167 + }; 168 + 169 + # Accepts pairing without PIN (NoInputNoOutput is just works) and service 170 + # connections of paired devices. 171 + systemd.services.bluetooth-agent = { 172 + wantedBy = [ "bluetooth.service" ]; 173 + bindsTo = [ "bluetooth.service" ]; 174 + after = [ "bluetooth.service" ]; 175 + serviceConfig = { 176 + ExecStart = "${pkgs.bluez-tools}/bin/bt-agent -c NoInputNoOutput"; 177 + DynamicUser = true; 178 + Restart = "always"; 179 + RestartSec = 2; 114 180 }; 115 181 }; 116 182 ··· 193 259 serviceConfig = { 194 260 DynamicUser = true; 195 261 SupplementaryGroups = [ "pipewire" ]; 262 + Restart = "always"; 263 + RestartSec = 2; 264 + }; 265 + }; 266 + 267 + # MQTT button -> pairing window. BlueZ itself turns discoverable/pairable 268 + # off after pairingTimeout; then devices bonded in the window get trusted, 269 + # so they reconnect without the agent, and the state goes back to OFF. 270 + systemd.services.bluetooth-pairing = { 271 + wantedBy = [ "bluetooth.service" ]; 272 + bindsTo = [ "bluetooth.service" ]; 273 + after = [ 274 + "bluetooth.service" 275 + "mosquitto.service" 276 + ]; 277 + path = [ 278 + pkgs.mosquitto 279 + config.systemd.package 280 + ]; 281 + script = '' 282 + ${lib.concatMapStrings (d: '' 283 + mosquitto_pub ${mqtt} -r -t ${d.topic} -m ${lib.escapeShellArg (builtins.toJSON d.config)} 284 + '') bluetoothDiscovery} 285 + adapter() { 286 + busctl set-property org.bluez /org/bluez/hci0 org.bluez.Adapter1 "$1" b "$2" 287 + } 288 + state() { 289 + mosquitto_pub ${mqtt} -r -t gg23/bluetooth-pairing/state -m "$1" 290 + } 291 + trust() { 292 + for d in $(busctl tree --list org.bluez | grep '/dev_[0-9A-F_]*$'); do 293 + if [ "$(busctl get-property org.bluez "$d" org.bluez.Device1 Bonded)" = "b true" ]; then 294 + busctl set-property org.bluez "$d" org.bluez.Device1 Trusted b true 295 + fi 296 + done 297 + } 298 + adapter Discoverable false 299 + adapter Pairable false 300 + trust 301 + state OFF 302 + mosquitto_sub ${mqtt} -t gg23/bluetooth-pairing/set | while read -r _; do 303 + if [ -n "''${window-}" ] && kill -0 "$window" 2>/dev/null; then 304 + continue 305 + fi 306 + adapter Pairable true 307 + adapter Discoverable true 308 + state ON 309 + (sleep ${toString pairingTimeout}; trust; state OFF) & 310 + window=$! 311 + done 312 + ''; 313 + serviceConfig = { 314 + DynamicUser = true; 196 315 Restart = "always"; 197 316 RestartSec = 2; 198 317 };