diff --git a/custom_components/nvidia_shield/connection.py b/custom_components/nvidia_shield/connection.py index 8290916..54875f8 100644 --- a/custom_components/nvidia_shield/connection.py +++ b/custom_components/nvidia_shield/connection.py @@ -228,7 +228,7 @@ class ShieldConnectionManager: keyfile=settings["keyfile"], ) - def _start_protocol(self) -> None: + async def _start_protocol(self) -> None: """Spin up the protocol transport alongside the ADB watchdog. One owner per transport: the library's own ``keep_reconnecting()`` @@ -245,6 +245,12 @@ class ShieldConnectionManager: client = self._protocol if client is None: return + # Real client: make sure local certificate material exists before it + # connects (fails soft — pair flows can regenerate). + generate_cert = getattr(client, "async_generate_cert_if_missing", None) + if generate_cert is not None: + with contextlib.suppress(Exception): + await generate_cert() self._protocol_loop = asyncio.get_running_loop() client.add_is_on_updated_callback(self._on_is_on) client.add_is_available_updated_callback(self._on_is_available) @@ -408,7 +414,7 @@ class ShieldConnectionManager: """ self._stopping = False self._tasks.append(self._create_task(self._watchdog(), "shield_adb_watchdog")) - self._start_protocol() + await self._start_protocol() async def async_stop(self) -> None: """Cancel all manager tasks and close the socket. diff --git a/custom_components/nvidia_shield/const.py b/custom_components/nvidia_shield/const.py index f0d7ad8..44599c0 100644 --- a/custom_components/nvidia_shield/const.py +++ b/custom_components/nvidia_shield/const.py @@ -22,6 +22,13 @@ CONF_ADB_STORE_APP_INFO = "adb_store_app_info" CONF_ADB_HOST = "adb_host" CONF_ADB_PORT = "adb_port" +# Remote-protocol transport (androidtvremote2) — Phase 2 +CONF_PROTOCOL_ENABLED = "protocol_enabled" +CONF_PROTOCOL_HOST = "protocol_host" +CONF_PAIRING_CODE = "pairing_code" +DEFAULT_PROTOCOL_API_PORT = 6466 +DEFAULT_PROTOCOL_PAIR_PORT = 6467 + ADB_POLL_INTERVAL = 10 # seconds ADB_RETRY_INTERVAL = 300 # seconds before retrying a failed ADB connection DEFAULT_ADB_PORT = 5555 diff --git a/tests/test_protocol_transport.py b/tests/test_protocol_transport.py index 58843f7..9420279 100644 --- a/tests/test_protocol_transport.py +++ b/tests/test_protocol_transport.py @@ -99,7 +99,7 @@ class TestPushCallbackWiring: notified = [] manager.register_listener(lambda: notified.append(True)) - manager._start_protocol() + await manager._start_protocol() client.push_is_on(True) client.push_is_available(False) await _drain_loop() @@ -114,7 +114,7 @@ class TestPushCallbackWiring: async def test_current_app_push_stored_raw_package(self): client = FakeProtocolClient() manager = _protocol_manager(client) - manager._start_protocol() + await manager._start_protocol() client.push_current_app("com.netflix.ninja") await _drain_loop() assert manager.current_app == "com.netflix.ninja" # name resolution deferred @@ -123,7 +123,7 @@ class TestPushCallbackWiring: async def test_volume_push_normalized_to_level_max_muted(self): client = FakeProtocolClient() manager = _protocol_manager(client) - manager._start_protocol() + await manager._start_protocol() client.push_volume_info({"level": 7, "max": 15, "muted": True}) await _drain_loop() assert manager.volume_info == (7.0, 15.0, True) @@ -138,7 +138,7 @@ class TestInvalidAuthHook: manager = _protocol_manager( client, invalid_auth_callback=lambda: fired.append(True) ) - manager._start_protocol() + await manager._start_protocol() client.fire_invalid_auth() @@ -148,7 +148,7 @@ class TestInvalidAuthHook: async def test_invalid_auth_without_callback_is_safe(self): client = FakeProtocolClient() manager = _protocol_manager(client) - manager._start_protocol() + await manager._start_protocol() client.fire_invalid_auth() # no callback registered — must not raise await manager.async_stop() @@ -159,7 +159,7 @@ class TestLifecycle: tears it down — the manager never wraps that task itself.""" client = FakeProtocolClient() manager = _protocol_manager(client) - manager._start_protocol() + await manager._start_protocol() task = client._reconnect_task assert task is not None @@ -189,7 +189,7 @@ class TestLifecycle: "keyfile": "/tmp/key.pem", }, ) - manager._start_protocol() + await manager._start_protocol() assert manager._protocol is client assert built @@ -202,7 +202,7 @@ class TestLifecycle: async def test_passive_without_protocol_configuration(self): manager = _protocol_manager(client=None) - manager._start_protocol() # no factory, no settings → no-op + await manager._start_protocol() # no factory, no settings → no-op assert manager._protocol is None assert manager.is_on is None @@ -220,7 +220,7 @@ class TestPushEdgeCases: notified = [] manager.register_listener(lambda: notified.append(True)) - manager._start_protocol() + await manager._start_protocol() loop = asyncio.new_event_loop() loop.close() @@ -238,7 +238,7 @@ class TestPushEdgeCases: store = StateStore(ttl=60.0, clock=lambda: now[0]) client = FakeProtocolClient() manager = _protocol_manager(client, store=store) - manager._start_protocol() + await manager._start_protocol() client.push_is_on(True) await _drain_loop() @@ -250,7 +250,7 @@ class TestPushEdgeCases: async def test_pushes_are_stamped_with_transition_time(self): client = FakeProtocolClient() manager = _protocol_manager(client) - manager._start_protocol() + await manager._start_protocol() before = time.monotonic() client.push_is_available(True) await _drain_loop()