Replace HTTP polling with WebSocket push notifications for instant state change responses. Server broadcasts updates only when significant changes occur (state, track, volume, etc.) while letting Home Assistant interpolate position during playback. Includes seek detection for timeline updates and automatic fallback to HTTP polling if WebSocket disconnects. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
165 lines
4.8 KiB
Python
165 lines
4.8 KiB
Python
"""The Remote Media Player integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import Platform
|
|
from homeassistant.core import HomeAssistant, ServiceCall
|
|
from homeassistant.helpers import config_validation as cv
|
|
|
|
from .api_client import MediaServerClient, MediaServerError
|
|
from .const import (
|
|
ATTR_SCRIPT_ARGS,
|
|
ATTR_SCRIPT_NAME,
|
|
CONF_HOST,
|
|
CONF_PORT,
|
|
CONF_TOKEN,
|
|
DOMAIN,
|
|
SERVICE_EXECUTE_SCRIPT,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.BUTTON]
|
|
|
|
# Service schema for execute_script
|
|
SERVICE_EXECUTE_SCRIPT_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_SCRIPT_NAME): cv.string,
|
|
vol.Optional(ATTR_SCRIPT_ARGS, default=[]): vol.All(
|
|
cv.ensure_list, [cv.string]
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up Remote Media Player from a config entry.
|
|
|
|
Args:
|
|
hass: Home Assistant instance
|
|
entry: Config entry
|
|
|
|
Returns:
|
|
True if setup was successful
|
|
"""
|
|
_LOGGER.debug("Setting up Remote Media Player: %s", entry.entry_id)
|
|
|
|
# Create API client
|
|
client = MediaServerClient(
|
|
host=entry.data[CONF_HOST],
|
|
port=entry.data[CONF_PORT],
|
|
token=entry.data[CONF_TOKEN],
|
|
)
|
|
|
|
# Verify connection
|
|
if not await client.check_connection():
|
|
_LOGGER.error("Failed to connect to Media Server")
|
|
await client.close()
|
|
return False
|
|
|
|
# Store client in hass.data
|
|
hass.data.setdefault(DOMAIN, {})
|
|
hass.data[DOMAIN][entry.entry_id] = {
|
|
"client": client,
|
|
}
|
|
|
|
# Register services if not already registered
|
|
if not hass.services.has_service(DOMAIN, SERVICE_EXECUTE_SCRIPT):
|
|
async def async_execute_script(call: ServiceCall) -> dict[str, Any]:
|
|
"""Execute a script on the media server."""
|
|
script_name = call.data[ATTR_SCRIPT_NAME]
|
|
script_args = call.data.get(ATTR_SCRIPT_ARGS, [])
|
|
|
|
_LOGGER.debug(
|
|
"Executing script '%s' with args: %s", script_name, script_args
|
|
)
|
|
|
|
# Get all clients and execute on all of them
|
|
results = {}
|
|
for entry_id, data in hass.data[DOMAIN].items():
|
|
client: MediaServerClient = data["client"]
|
|
try:
|
|
result = await client.execute_script(script_name, script_args)
|
|
results[entry_id] = result
|
|
_LOGGER.info(
|
|
"Script '%s' executed on %s: success=%s",
|
|
script_name,
|
|
entry_id,
|
|
result.get("success", False),
|
|
)
|
|
except MediaServerError as err:
|
|
_LOGGER.error(
|
|
"Failed to execute script '%s' on %s: %s",
|
|
script_name,
|
|
entry_id,
|
|
err,
|
|
)
|
|
results[entry_id] = {"success": False, "error": str(err)}
|
|
|
|
return results
|
|
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_EXECUTE_SCRIPT,
|
|
async_execute_script,
|
|
schema=SERVICE_EXECUTE_SCRIPT_SCHEMA,
|
|
)
|
|
|
|
# Forward setup to platforms
|
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
|
|
# Register update listener for options
|
|
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
|
|
|
return True
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Unload a config entry.
|
|
|
|
Args:
|
|
hass: Home Assistant instance
|
|
entry: Config entry
|
|
|
|
Returns:
|
|
True if unload was successful
|
|
"""
|
|
_LOGGER.debug("Unloading Remote Media Player: %s", entry.entry_id)
|
|
|
|
# Unload platforms
|
|
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
|
|
if unload_ok:
|
|
# Close client and remove data
|
|
data = hass.data[DOMAIN].pop(entry.entry_id)
|
|
|
|
# Shutdown coordinator (WebSocket cleanup)
|
|
if "coordinator" in data:
|
|
await data["coordinator"].async_shutdown()
|
|
|
|
# Close HTTP client
|
|
await data["client"].close()
|
|
|
|
# Remove services if this was the last entry
|
|
if not hass.data[DOMAIN]:
|
|
hass.services.async_remove(DOMAIN, SERVICE_EXECUTE_SCRIPT)
|
|
|
|
return unload_ok
|
|
|
|
|
|
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
"""Handle options update.
|
|
|
|
Args:
|
|
hass: Home Assistant instance
|
|
entry: Config entry
|
|
"""
|
|
_LOGGER.debug("Options updated for: %s", entry.entry_id)
|
|
await hass.config_entries.async_reload(entry.entry_id)
|