From a2e40884a7e39a733a526ab91a9396b20a7208a5 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 2 Dec 2025 20:09:25 +0100 Subject: [PATCH] Finalize Remote Access code (for now) --- .../webserver/remote_access/__init__.py | 164 +++++------------- .../webserver/remote_access/gateway.py | 3 +- 2 files changed, 45 insertions(+), 122 deletions(-) diff --git a/music_assistant/controllers/webserver/remote_access/__init__.py b/music_assistant/controllers/webserver/remote_access/__init__.py index 281bd07d..058f2ee3 100644 --- a/music_assistant/controllers/webserver/remote_access/__init__.py +++ b/music_assistant/controllers/webserver/remote_access/__init__.py @@ -1,11 +1,8 @@ -""" -Remote Access subcomponent for the Webserver Controller. +"""Remote Access subcomponent for the Webserver Controller. This module manages WebRTC-based remote access to Music Assistant instances. It connects to a signaling server and handles incoming WebRTC connections, bridging them to the local WebSocket API. - -Requires an active Home Assistant Cloud subscription due to STUN/TURN/SIGNALING server usage. """ from __future__ import annotations @@ -15,14 +12,12 @@ from typing import TYPE_CHECKING, cast from mashumaro import DataClassDictMixin from music_assistant_models.enums import EventType -from music_assistant_models.errors import UnsupportedFeaturedException from music_assistant.constants import CONF_CORE from music_assistant.controllers.webserver.remote_access.gateway import ( WebRTCGateway, generate_remote_id, ) -from music_assistant.helpers.api import api_command if TYPE_CHECKING: from music_assistant_models.event import MassEvent @@ -33,7 +28,6 @@ if TYPE_CHECKING: # Signaling server URL SIGNALING_SERVER_URL = "wss://signaling.music-assistant.io/ws" -# Storage keys CONF_KEY_MAIN = "remote_access" CONF_REMOTE_ID = "remote_id" CONF_ENABLED = "enabled" @@ -47,7 +41,7 @@ class RemoteAccessInfo(DataClassDictMixin): running: bool connected: bool remote_id: str - ha_cloud_available: bool + using_ha_cloud: bool signaling_url: str @@ -62,11 +56,10 @@ class RemoteAccessManager: self.gateway: WebRTCGateway | None = None self._remote_id: str | None = None self._enabled: bool = False - self._ha_cloud_available: bool = False + self._using_ha_cloud: bool = False async def setup(self) -> None: """Initialize the remote access manager.""" - # Load config from storage enabled_value = self.mass.config.get(f"{CONF_CORE}/{CONF_KEY_MAIN}/{CONF_ENABLED}", False) self._enabled = bool(enabled_value) remote_id_value = self.mass.config.get( @@ -75,16 +68,11 @@ class RemoteAccessManager: if not remote_id_value: remote_id_value = generate_remote_id() self.mass.config.set(f"{CONF_CORE}/{CONF_KEY_MAIN}/{CONF_REMOTE_ID}", remote_id_value) - self.logger.debug("Generated new Remote ID: %s", remote_id_value) self._remote_id = str(remote_id_value) self._register_api_commands() - # Subscribe to provider updates to check for Home Assistant Cloud - self._ha_cloud_available = await self._check_ha_cloud_status() - self.mass.subscribe( - self._on_providers_updated, EventType.PROVIDERS_UPDATED, id_filter="hass" - ) - if self._enabled and self._ha_cloud_available: + self.mass.subscribe(self._on_providers_updated, EventType.PROVIDERS_UPDATED) + if self._enabled: await self.start() async def close(self) -> None: @@ -93,40 +81,21 @@ class RemoteAccessManager: async def start(self) -> None: """Start the remote access gateway.""" - if self.is_running: - self.logger.debug("Remote access already running") - return - if not self._ha_cloud_available: - raise UnsupportedFeaturedException( - "Home Assistant Cloud subscription is required for remote access" - ) - if not self._enabled: - # should not happen, but guard anyway - self.logger.debug("Remote access is disabled in configuration") + if self.is_running or not self._enabled: return - self.logger.info("Starting remote access with Remote ID: %s", self._remote_id) - - # Determine local WebSocket URL from webserver config base_url = self.mass.webserver.base_url local_ws_url = base_url.replace("http", "ws") if not local_ws_url.endswith("/"): local_ws_url += "/" local_ws_url += "ws" - # Get ICE servers from HA Cloud if available - ice_servers: list[dict[str, str]] | None = None - if await self._check_ha_cloud_status(): - self.logger.info( - "Home Assistant Cloud subscription detected, using HA cloud ICE servers" - ) - ice_servers = await self._get_ha_cloud_ice_servers() - else: - self.logger.info( - "Home Assistant Cloud subscription not detected, using default STUN servers" - ) + ha_cloud_available, ice_servers = await self._get_ha_cloud_status() + self._using_ha_cloud = bool(ha_cloud_available and ice_servers) + + mode = "optimized" if self._using_ha_cloud else "basic" + self.logger.info("Starting remote access in %s mode (ID: %s)", mode, self._remote_id) - # Initialize and start the WebRTC gateway self.gateway = WebRTCGateway( http_session=self.mass.http_session, signaling_url=SIGNALING_SERVER_URL, @@ -143,85 +112,49 @@ class RemoteAccessManager: if self.gateway: await self.gateway.stop() self.gateway = None - self.logger.debug("WebRTC Remote Access stopped") async def _on_providers_updated(self, event: MassEvent) -> None: - """ - Handle providers updated event. + """Handle providers updated event to detect HA Cloud status changes. :param event: The providers updated event. """ - last_ha_cloud_available = self._ha_cloud_available - self._ha_cloud_available = await self._check_ha_cloud_status() - if self._ha_cloud_available == last_ha_cloud_available: - return # No change in HA Cloud status - if self.is_running and not self._ha_cloud_available: - self.logger.warning( - "Home Assistant Cloud subscription is no longer active, stopping remote access" - ) - await self.stop() + if not self.is_running or not self._enabled: return - allow_start = self._ha_cloud_available and self._enabled - if allow_start and self.is_running: - return # Already running - if allow_start: + + ha_cloud_available, ice_servers = await self._get_ha_cloud_status() + new_using_ha_cloud = bool(ha_cloud_available and ice_servers) + + if new_using_ha_cloud != self._using_ha_cloud: + self.logger.info("HA Cloud status changed, restarting remote access") + await self.stop() self.mass.create_task(self.start()) - async def _check_ha_cloud_status(self) -> bool: - """Check if Home Assistant Cloud subscription is active. + async def _get_ha_cloud_status(self) -> tuple[bool, list[dict[str, str]] | None]: + """Get Home Assistant Cloud status and ICE servers. - :return: True if HA Cloud is logged in and has active subscription. + :return: Tuple of (ha_cloud_available, ice_servers). """ - # Find the Home Assistant provider ha_provider = cast("HomeAssistantProvider | None", self.mass.get_provider("hass")) if not ha_provider: - return False + return False, None try: - # Access the hass client from the provider hass_client = ha_provider.hass if not hass_client or not hass_client.connected: - return False + return False, None - # Call cloud/status command to check subscription result = await hass_client.send_command("cloud/status") - - # Check for logged_in and active_subscription logged_in = result.get("logged_in", False) active_subscription = result.get("active_subscription", False) - return bool(logged_in and active_subscription) - - except Exception: - return False - - async def _get_ha_cloud_ice_servers(self) -> list[dict[str, str]] | None: - """Get ICE servers from Home Assistant Cloud. + if not (logged_in and active_subscription): + return False, None - :return: List of ICE server configurations or None if unavailable. - """ - # Find the Home Assistant provider - ha_provider = cast("HomeAssistantProvider | None", self.mass.get_provider("hass")) - if not ha_provider: - return None - - try: - hass_client = ha_provider.hass - if not hass_client or not hass_client.connected: - return None - - # Try to get ICE servers from HA Cloud - # This might be available via a cloud API endpoint - # For now, return None and use default STUN servers - # TODO: Research if HA Cloud exposes ICE/TURN server endpoints - self.logger.debug( - "Using default STUN servers (HA Cloud ICE servers not yet implemented)" - ) - return None + return True, None except Exception: - self.logger.exception("Error getting Home Assistant Cloud ICE servers") - return None + self.logger.exception("Error getting HA Cloud status") + return False, None @property def is_enabled(self) -> bool: @@ -246,42 +179,31 @@ class RemoteAccessManager: def _register_api_commands(self) -> None: """Register API commands for remote access.""" - @api_command("remote_access/info") - def get_remote_access_info() -> RemoteAccessInfo: - """Get remote access information. - - Returns information about the remote access configuration including - whether it's enabled, running status, connected status, and the Remote ID. - """ + async def get_remote_access_info() -> RemoteAccessInfo: + """Get remote access information.""" return RemoteAccessInfo( enabled=self.is_enabled, running=self.is_running, connected=self.is_connected, remote_id=self._remote_id or "", - ha_cloud_available=self._ha_cloud_available, + using_ha_cloud=self._using_ha_cloud, signaling_url=SIGNALING_SERVER_URL, ) - @api_command("remote_access/configure", required_role="admin") - async def configure_remote_access( - enabled: bool, - ) -> RemoteAccessInfo: - """ - Configure remote access settings. + async def configure_remote_access(enabled: bool) -> RemoteAccessInfo: + """Configure remote access settings. :param enabled: Enable or disable remote access. - - Starts or stops the WebRTC gateway based on the enabled parameter. - Returns the updated remote access info. """ - # Save configuration self._enabled = enabled self.mass.config.set(f"{CONF_CORE}/{CONF_KEY_MAIN}/{CONF_ENABLED}", enabled) - allow_start = self._ha_cloud_available and self._enabled - - # Start or stop the gateway based on enabled flag - if allow_start and not self.is_running: + if self._enabled and not self.is_running: await self.start() - elif not allow_start and self.is_running: + elif not self._enabled and self.is_running: await self.stop() - return get_remote_access_info() + return await get_remote_access_info() + + self.mass.register_api_command("remote_access/info", get_remote_access_info) + self.mass.register_api_command( + "remote_access/configure", configure_remote_access, required_role="admin" + ) diff --git a/music_assistant/controllers/webserver/remote_access/gateway.py b/music_assistant/controllers/webserver/remote_access/gateway.py index 80d82b8b..39351b58 100644 --- a/music_assistant/controllers/webserver/remote_access/gateway.py +++ b/music_assistant/controllers/webserver/remote_access/gateway.py @@ -88,6 +88,7 @@ class WebRTCGateway: self.logger = LOGGER self.ice_servers = ice_servers or [ + {"urls": "stun:stun.home-assistant.io:3478"}, {"urls": "stun:stun.l.google.com:19302"}, {"urls": "stun:stun1.l.google.com:19302"}, {"urls": "stun:stun.cloudflare.com:3478"}, @@ -246,7 +247,7 @@ class WebRTCGateway: "type": "register-server", "remoteId": self.remote_id, } - self.logger.info( + self.logger.debug( "Sending registration to signaling server with Remote ID: %s", self.remote_id, ) -- 2.34.1