Fix empty Bluetooth lists in web mixer; advertise it over Bonjour

busctl cannot convert some BlueZ advertisement data (Apple
ManufacturerData, a{qv}) to JSON, so GetManagedObjects failed as soon as
an iPad was paired and the web showed no devices at all. Read only the
needed Device1/Adapter1 properties per object instead. Also publish the
web mixer as an _http._tcp Avahi service.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jsahn
2026-10-12 21:54:38 +09:00
co-authored by Claude Opus 5.5
parent 5e8906b43d
commit 7fa372e801
5 changed files with 63 additions and 8 deletions
+25 -5
View File
@@ -35,6 +35,22 @@ def objects(service, path):
for p, interfaces in raw.items()}
def properties(service, path, interface, names):
"""Read selected properties only.
GetManagedObjects cannot be used for BlueZ: busctl fails to convert some
advertisement data (e.g. Apple ManufacturerData a{qv}) to JSON, which
would hide every device.
"""
out = command('busctl', '--system', '--json=short', 'get-property', service, path, interface, *names)
return {name: json.loads(line)['data'] for name, line in zip(names, out.splitlines())}
def bluez_paths(pattern):
return [p for p in command('busctl', '--system', 'tree', '--list', 'org.bluez').split()
if re.fullmatch(pattern, p)]
def pcms():
return {p: interfaces['org.bluealsa.PCM1'] for p, interfaces in objects('org.bluealsa', '/org/bluealsa').items()
if 'org.bluealsa.PCM1' in interfaces}
@@ -141,9 +157,13 @@ def set_input(key, volume=None, mute=None):
def devices():
result = []
for path, interfaces in objects('org.bluez', '/').items():
props = interfaces.get('org.bluez.Device1')
if not props: continue
base = ['Address', 'Alias', 'Paired', 'Bonded', 'Connected']
for path in bluez_paths(r'/org/bluez/hci\d+/dev_[0-9A-F_]{17}'):
try:
try: props = properties('org.bluez', path, 'org.bluez.Device1', base + ['UUIDs'])
# UUIDs is absent until services are known; devices may also vanish mid-read.
except ValueError: props = properties('org.bluez', path, 'org.bluez.Device1', base)
except ValueError: continue
result.append({'address': props['Address'],'name': props.get('Alias',props['Address']),
'bonded': bool(props.get('Bonded')), 'paired': bool(props.get('Paired')),'connected':bool(props.get('Connected')),
'output': '0000110b-0000-1000-8000-00805f9b34fb' in props.get('UUIDs',[])})
@@ -151,8 +171,8 @@ def devices():
def pairing():
adapter = next((i['org.bluez.Adapter1'] for i in objects('org.bluez', '/').values()
if 'org.bluez.Adapter1' in i), {})
paths = bluez_paths(r'/org/bluez/hci\d+')
adapter = properties('org.bluez', paths[0], 'org.bluez.Adapter1', ['Discoverable', 'Alias']) if paths else {}
on = bool(adapter.get('Discoverable'))
remaining = 0
try: remaining = max(0, round(float(PAIRING_UNTIL.read_text()) - time.time()))