Add in-app speaker discovery and connection guide
This commit is contained in:
7 files changed
+287
-28
No files matched your search
@@ -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", []))
|
||||
@@ -9,7 +9,7 @@ class ImportTests(unittest.TestCase):
|
||||
def test_headless_import_and_help(self):
|
||||
script = """
|
||||
import sys
|
||||
from controller import protocol, session, messages, cli
|
||||
from controller import protocol, session, messages, discovery, cli
|
||||
assert 'tkinter' not in sys.modules
|
||||
assert 'bleak' not in sys.modules
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user