Finalize Remote Access code (for now)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 2 Dec 2025 19:09:25 +0000 (20:09 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 2 Dec 2025 19:09:25 +0000 (20:09 +0100)
music_assistant/controllers/webserver/remote_access/__init__.py
music_assistant/controllers/webserver/remote_access/gateway.py

index 281bd07dcf9a99ca0bee12e10f174a7f162846fa..058f2ee3a7a1931f6bfdaba801158562f5e8702c 100644 (file)
@@ -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"
+        )
index 80d82b8b261488c2d29cff0338ee1e8003129622..39351b58e668f66fcec94e981b01e56834f06173 100644 (file)
@@ -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,
             )