Add in-app speaker discovery and connection guide

This commit is contained in:
lda committed 2026-10-25 04:59:07 +07:00 Verified
1 parent 038e152bbc
commit f75d3fb558
7 files changed
+287 -28

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
"""Discovery fixtures include the owner's actual unnamed FE40 advertisement."""
import asyncio
import queue
import sys
import types
import unittest
from unittest.mock import AsyncMock, patch
from controller.discovery import ScanWorker, Speaker, scan_speakers, selected_address
def advertisement(name, address, uuids=()):
return (
types.SimpleNamespace(name=name, address=address),
types.SimpleNamespace(local_name=name, service_uuids=list(uuids)),
)
class DiscoveryTests(unittest.TestCase):
def test_unnamed_model_advertisement_and_named_speaker_are_selectable(self):
found = {
"unnamed": advertisement(
None, "12:34:56:78:9A:BC", ["0000fe40-0000-1000-8000-00805f9b34fb"]
),
"named": advertisement("[AV] MX-T40", "12:34:56:78:9A:BD"),
"unrelated": advertisement(
"Headphones", "12:34:56:78:9A:BE", ["0000ffa0-0000-1000-8000-00805f9b34fb"]
),
}
discover = AsyncMock(return_value=found)
with patch.dict(
sys.modules,
{"bleak": types.SimpleNamespace(BleakScanner=types.SimpleNamespace(discover=discover))},
):
speakers = asyncio.run(scan_speakers(timeout=3))
self.assertEqual(len(speakers), 2)
self.assertEqual(
{speaker.address for speaker in speakers}, {"12:34:56:78:9A:BC", "12:34:56:78:9A:BD"}
)
unnamed = next(speaker for speaker in speakers if speaker.address.endswith("BC"))
self.assertEqual(unnamed.name, "MX-T40 (model advertisement)")
self.assertIn(unnamed.address, unnamed.label)
discover.assert_awaited_once_with(timeout=3, return_adv=True)
def test_choice_resolves_to_address_and_preserves_manual_input(self):
speaker = Speaker("12:34:56:78:9A:BC", "MX-T40")
self.assertEqual(selected_address(speaker.label, [speaker]), speaker.address)
self.assertEqual(selected_address(" 12:34:56:78:9A:BD ", [speaker]), "12:34:56:78:9A:BD")
def test_identical_names_remain_distinguishable(self):
speakers = [Speaker("12:34:56:78:9A:BC", "MX-T40"), Speaker("12:34:56:78:9A:BD", "MX-T40")]
self.assertNotEqual(speakers[0].label, speakers[1].label)
self.assertEqual(selected_address(speakers[1].label, speakers), speakers[1].address)
def test_scan_error_is_delivered_to_gui_queue(self):
events = queue.Queue()
with patch(
"controller.discovery.scan_speakers",
AsyncMock(side_effect=RuntimeError("Bluetooth is off")),
):
ScanWorker(events).run()
self.assertEqual(events.get_nowait(), ("scan_error", "Bluetooth is off"))
def test_empty_scan_has_a_result_event(self):
events = queue.Queue()
with patch("controller.discovery.scan_speakers", AsyncMock(return_value=[])):
ScanWorker(events).run()
self.assertEqual(events.get_nowait(), ("scan_results", []))