feat(displays): per-display devices + DDC/CI capability entities

Restructure how displays are exposed in Home Assistant:

Each physical monitor is now its own HA device linked to the media-server
hub via `via_device`. The hub keeps the media_player + script buttons; per-
display devices hold the power switch, brightness slider, and the new
capability entities. This lets users place displays in their own area/room
and keeps related entities grouped together in the UI.

New platforms:
- sensor: DisplayResolutionSensor (diagnostic, from EDID)
- binary_sensor: DisplayPrimaryBinarySensor + DisplayPowerControlBinarySensor
  (both diagnostic; help users see why a power switch is or isn't created)
- select: DisplayInputSourceSelect (HDMI1/DP1/...), DisplayColorPresetSelect
  (color temperature), DisplayPictureModeSelect (VCP 0xDC scene modes)
- number: added DisplayContrastNumber alongside brightness

Other changes:
- display_device helper centralises the per-display DeviceInfo; pulls real
  manufacturer/model from EDID; device name no longer prepends the hub
  title since via_device already shows the hierarchy.
- api_client gains set_display_{contrast,input_source,color_preset,picture_mode}
  and stops forcing `?refresh=true` on every poll so HA can ride the
  server's TTL cache instead of triggering full DDC/CI probes per entity.
- select / number entities now check the server's `success` flag and re-
  sync from the actual monitor state when a write was silently rejected
  (some monitors honor reads but ignore writes for certain DDC/CI codes).

Bumps manifest.json to 0.3.0 - the device topology change is user-visible
and existing brightness/power entities migrate to per-display devices on
first reload (unique_ids are preserved).
This commit is contained in:
2026-05-15 14:46:50 +03:00
parent b0d98a9d45
commit 4156dedf5e
10 changed files with 583 additions and 58 deletions
@@ -33,6 +33,10 @@ from .const import (
API_DISPLAY_MONITORS,
API_DISPLAY_BRIGHTNESS,
API_DISPLAY_POWER,
API_DISPLAY_CONTRAST,
API_DISPLAY_INPUT_SOURCE,
API_DISPLAY_COLOR_PRESET,
API_DISPLAY_PICTURE_MODE,
)
_LOGGER = logging.getLogger(__name__)
@@ -348,12 +352,12 @@ class MediaServerClient:
return await self._request("POST", API_BROWSER_PLAY, {"path": file_path})
async def get_display_monitors(self) -> list[dict[str, Any]]:
"""Get list of connected monitors with brightness and power info.
"""Get list of connected monitors with brightness, power, DDC/CI state.
Returns:
List of monitor dicts with id, name, brightness, power_supported, power_on, resolution
Uses the server's short TTL cache so per-entity polling does not pay
the full DDC/CI probe cost on every call.
"""
return await self._request("GET", f"{API_DISPLAY_MONITORS}?refresh=true")
return await self._request("GET", API_DISPLAY_MONITORS)
async def set_display_brightness(self, monitor_id: int, brightness: int) -> dict[str, Any]:
"""Set brightness for a specific monitor.
@@ -383,6 +387,30 @@ class MediaServerClient:
"POST", f"{API_DISPLAY_POWER}/{monitor_id}", {"on": on}
)
async def set_display_contrast(self, monitor_id: int, contrast: int) -> dict[str, Any]:
"""Set DDC/CI contrast for a specific monitor (0-100)."""
return await self._request(
"POST", f"{API_DISPLAY_CONTRAST}/{monitor_id}", {"contrast": contrast}
)
async def set_display_input_source(self, monitor_id: int, source: str) -> dict[str, Any]:
"""Switch a monitor's DDC/CI input source by enum name (e.g. 'HDMI1')."""
return await self._request(
"POST", f"{API_DISPLAY_INPUT_SOURCE}/{monitor_id}", {"source": source}
)
async def set_display_color_preset(self, monitor_id: int, preset: str) -> dict[str, Any]:
"""Apply a DDC/CI color preset by enum name (e.g. 'COLOR_TEMP_6500K')."""
return await self._request(
"POST", f"{API_DISPLAY_COLOR_PRESET}/{monitor_id}", {"preset": preset}
)
async def set_display_picture_mode(self, monitor_id: int, code: int) -> dict[str, Any]:
"""Apply a DDC/CI picture/scene mode (VCP 0xDC) by raw code."""
return await self._request(
"POST", f"{API_DISPLAY_PICTURE_MODE}/{monitor_id}", {"code": code}
)
class MediaServerWebSocket:
"""WebSocket client for real-time media status updates."""