feat: shared DisplayCoordinator + optional API token
- Introduce DisplayCoordinator polling /api/display/monitors once per cycle and fan out to all per-display entities via CoordinatorEntity. Removes ~9x redundant requests per polling cycle that came from each binary_sensor/number/select/sensor/switch entity calling get_display_monitors() in its own async_update. - Optimistic write-through via coordinator.apply_optimistic(...) keeps sibling entities in sync after slider/select writes without an extra network round-trip. - Make CONF_TOKEN optional. The media server already supports running without auth (auth_enabled() returns False when api_tokens is empty), so the integration omits the Authorization header and ?token= query from REST/WS/album-art URLs when no token is configured. Server-side auth-enabled rejections still surface as invalid_auth in the UI. - Bump manifest version to 0.3.2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,11 +92,16 @@ class MediaServerClient:
|
||||
await self._session.close()
|
||||
|
||||
def _get_headers(self) -> dict[str, str]:
|
||||
"""Get headers for API requests."""
|
||||
return {
|
||||
"Authorization": f"Bearer {self._token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
"""Get headers for API requests.
|
||||
|
||||
When no token is configured the media server runs in anonymous mode
|
||||
(``auth.auth_enabled()`` returns False), so we omit the Authorization
|
||||
header entirely rather than sending ``Bearer `` with an empty value.
|
||||
"""
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if self._token:
|
||||
headers["Authorization"] = f"Bearer {self._token}"
|
||||
return headers
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
@@ -178,13 +183,17 @@ class MediaServerClient:
|
||||
"""
|
||||
data = await self._request("GET", API_STATUS)
|
||||
|
||||
# Convert relative album_art_url to absolute URL with token and cache-buster
|
||||
# Convert relative album_art_url to absolute URL with cache-buster
|
||||
# (and token only when auth is enabled on the server side).
|
||||
if data.get("album_art_url") and data["album_art_url"].startswith("/"):
|
||||
# Add track info hash to force HA to re-fetch when track changes
|
||||
import hashlib
|
||||
track_id = f"{data.get('title', '')}-{data.get('artist', '')}"
|
||||
track_hash = hashlib.md5(track_id.encode()).hexdigest()[:8]
|
||||
data["album_art_url"] = f"{self._base_url}{data['album_art_url']}?token={self._token}&t={track_hash}"
|
||||
token_param = f"token={self._token}&" if self._token else ""
|
||||
data["album_art_url"] = (
|
||||
f"{self._base_url}{data['album_art_url']}?{token_param}t={track_hash}"
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@@ -440,7 +449,11 @@ class MediaServerWebSocket:
|
||||
self._on_status_update = on_status_update
|
||||
self._on_disconnect = on_disconnect
|
||||
self._on_scripts_changed = on_scripts_changed
|
||||
self._ws_url = f"ws://{host}:{self._port}/api/media/ws?token={token}"
|
||||
# The server's WS endpoint accepts an unauthenticated connection when
|
||||
# api_tokens is empty (see media.py:websocket_endpoint), so we only
|
||||
# append ?token=... when one was configured.
|
||||
token_query = f"?token={token}" if token else ""
|
||||
self._ws_url = f"ws://{host}:{self._port}/api/media/ws{token_query}"
|
||||
self._session: aiohttp.ClientSession | None = None
|
||||
self._ws: aiohttp.ClientWebSocketResponse | None = None
|
||||
self._receive_task: asyncio.Task | None = None
|
||||
@@ -514,9 +527,10 @@ class MediaServerWebSocket:
|
||||
):
|
||||
track_id = f"{status_data.get('title', '')}-{status_data.get('artist', '')}"
|
||||
track_hash = hashlib.md5(track_id.encode()).hexdigest()[:8]
|
||||
token_param = f"token={self._token}&" if self._token else ""
|
||||
status_data["album_art_url"] = (
|
||||
f"http://{self._host}:{self._port}"
|
||||
f"{status_data['album_art_url']}?token={self._token}&t={track_hash}"
|
||||
f"{status_data['album_art_url']}?{token_param}t={track_hash}"
|
||||
)
|
||||
self._on_status_update(status_data)
|
||||
elif msg_type == "scripts_changed":
|
||||
|
||||
Reference in New Issue
Block a user