Show Bluetooth inputs when BlueZ objects can't be read as JSON

inputs() still used GetManagedObjects on BlueZ to look up names and
silently dropped every Bluetooth input when an Apple device's
advertisement data broke the JSON conversion. Read only Alias per
device and fall back to the address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jsahn
2026-10-12 22:01:54 +09:00
co-authored by Claude Opus 5.5
parent 7fa372e801
commit 1cd00ae21b
2 changed files with 12 additions and 3 deletions
+4 -3
View File
@@ -114,13 +114,14 @@ def inputs():
result.append({'id':'airplay','name':'AirPlay','kind':'airplay','volume':0, result.append({'id':'airplay','name':'AirPlay','kind':'airplay','volume':0,
'muted':False,'playing':False,'available':False}) 'muted':False,'playing':False,'available':False})
try: try:
devices = objects('org.bluez', '/')
for path, props in pcms().items(): for path, props in pcms().items():
if not is_input(props): continue if not is_input(props): continue
dev = props['Device']; key = address(dev) dev = props['Device']; key = address(dev)
d = devices.get(dev, {}).get('org.bluez.Device1', {}) # A missing name must not hide a working input.
try: name = properties('org.bluez', dev, 'org.bluez.Device1', ['Alias'])['Alias']
except (ValueError, KeyError, subprocess.TimeoutExpired): name = key
volume, muted = decode_volume(int(props['Volume'])) volume, muted = decode_volume(int(props['Volume']))
result.append({'id':key,'name':d.get('Alias', key),'kind':'bluetooth','volume':volume, result.append({'id':key,'name':name,'kind':'bluetooth','volume':volume,
'muted':muted,'playing':bool(props.get('Running')),'available':True}) 'muted':muted,'playing':bool(props.get('Running')),'available':True})
except (ValueError, KeyError, subprocess.TimeoutExpired): pass except (ValueError, KeyError, subprocess.TimeoutExpired): pass
return result return result
+8
View File
@@ -58,6 +58,14 @@ class WebTests(unittest.TestCase):
{'action':'discoverable','on':'yes','seconds':300},{'action':'discoverable','on':True,'seconds':True}]: {'action':'discoverable','on':'yes','seconds':300},{'action':'discoverable','on':True,'seconds':True}]:
with self.subTest(data=data), self.assertRaises(ValueError): w.validate_action(data) with self.subTest(data=data), self.assertRaises(ValueError): w.validate_action(data)
def test_bluetooth_input_listed_even_if_name_unreadable(self):
source={'Device':'/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF','Mode':'source','Transport':'A2DP-sink','Volume':0x4040,'Running':True}
with patch.object(a,'airplay',side_effect=ValueError), patch.object(a,'pcms',return_value={'/input':source}), \
patch.object(a,'properties',side_effect=ValueError('json')):
items=a.inputs()
self.assertEqual([(i['id'],i['name'],i['playing']) for i in items if i['kind']=='bluetooth'],
[('AA:BB:CC:DD:EE:FF','AA:BB:CC:DD:EE:FF',True)])
def test_devices_skip_unreadable_and_tolerate_missing_uuids(self): def test_devices_skip_unreadable_and_tolerate_missing_uuids(self):
out={'/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF':'{"type":"s","data":"AA:BB:CC:DD:EE:FF"}\n{"type":"s","data":"QCY"}\n' out={'/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF':'{"type":"s","data":"AA:BB:CC:DD:EE:FF"}\n{"type":"s","data":"QCY"}\n'
'{"type":"b","data":true}\n{"type":"b","data":true}\n{"type":"b","data":false}'} '{"type":"b","data":true}\n{"type":"b","data":true}\n{"type":"b","data":false}'}