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:
2026-05-17 23:46:26 +03:00
parent 68e338de4e
commit ab0585278c
14 changed files with 313 additions and 252 deletions
+44 -48
View File
@@ -9,9 +9,11 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .api_client import MediaServerClient, MediaServerError
from .const import DOMAIN
from .display_coordinator import DisplayCoordinator
from .display_device import display_device_info
_LOGGER = logging.getLogger(__name__)
@@ -23,21 +25,16 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up display power switch entities from a config entry."""
client: MediaServerClient = hass.data[DOMAIN][entry.entry_id]["client"]
data = hass.data[DOMAIN][entry.entry_id]
client: MediaServerClient = data["client"]
coordinator: DisplayCoordinator = data["display_coordinator"]
try:
monitors = await client.get_display_monitors()
except MediaServerError as err:
_LOGGER.error("Failed to fetch display monitors: %s", err)
if not coordinator.data:
return
entities = [
DisplayPowerSwitch(
client=client,
entry=entry,
monitor=monitor,
)
for monitor in monitors
DisplayPowerSwitch(coordinator, client, entry, monitor)
for monitor in coordinator.data.values()
if monitor.get("power_supported", False)
]
@@ -46,7 +43,7 @@ async def async_setup_entry(
_LOGGER.info("Added %d display power switch entities", len(entities))
class DisplayPowerSwitch(SwitchEntity):
class DisplayPowerSwitch(CoordinatorEntity[DisplayCoordinator], SwitchEntity):
"""Switch entity for controlling display power."""
_attr_has_entity_name = True
@@ -55,54 +52,53 @@ class DisplayPowerSwitch(SwitchEntity):
def __init__(
self,
coordinator: DisplayCoordinator,
client: MediaServerClient,
entry: ConfigEntry,
monitor: dict[str, Any],
) -> None:
"""Initialize the display power switch."""
super().__init__(coordinator)
self._client = client
self._entry = entry
self._monitor_id: int = monitor["id"]
self._attr_is_on = monitor.get("power_on", True)
self._attr_unique_id = f"{entry.entry_id}_display_power_{self._monitor_id}"
self._attr_device_info = display_device_info(entry, monitor)
@property
def _monitor(self) -> dict[str, Any]:
if self.coordinator.data is None:
return {}
return self.coordinator.data.get(self._monitor_id, {})
@property
def is_on(self) -> bool:
return bool(self._monitor.get("power_on", True))
@property
def icon(self) -> str:
"""Return icon based on power state."""
return "mdi:monitor" if self._attr_is_on else "mdi:monitor-off"
return "mdi:monitor" if self.is_on else "mdi:monitor-off"
async def _set_power(self, on: bool) -> None:
try:
result = await self._client.set_display_power(self._monitor_id, on)
except MediaServerError as err:
_LOGGER.error(
"Failed to %s monitor %d: %s",
"turn on" if on else "turn off",
self._monitor_id,
err,
)
return
if not result.get("success"):
_LOGGER.error(
"Failed to %s monitor %d",
"turn on" if on else "turn off",
self._monitor_id,
)
return
self.coordinator.apply_optimistic(self._monitor_id, power_on=on)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the monitor on."""
try:
result = await self._client.set_display_power(self._monitor_id, True)
if result.get("success"):
self._attr_is_on = True
self.async_write_ha_state()
else:
_LOGGER.error("Failed to turn on monitor %d", self._monitor_id)
except MediaServerError as err:
_LOGGER.error("Failed to turn on monitor %d: %s", self._monitor_id, err)
await self._set_power(True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the monitor off."""
try:
result = await self._client.set_display_power(self._monitor_id, False)
if result.get("success"):
self._attr_is_on = False
self.async_write_ha_state()
else:
_LOGGER.error("Failed to turn off monitor %d", self._monitor_id)
except MediaServerError as err:
_LOGGER.error("Failed to turn off monitor %d: %s", self._monitor_id, err)
async def async_update(self) -> None:
"""Fetch updated power state from the server."""
try:
monitors = await self._client.get_display_monitors()
for monitor in monitors:
if monitor["id"] == self._monitor_id:
self._attr_is_on = monitor.get("power_on", True)
break
except MediaServerError as err:
_LOGGER.error("Failed to update power state for monitor %d: %s", self._monitor_id, err)
await self._set_power(False)