From 83153dbddda777d16574cf9f1a97b5f8a94b47f9 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 28 Feb 2026 12:10:48 +0300 Subject: [PATCH] Add display monitor brightness and power control entities - Add NUMBER platform for monitor brightness (0-100) - Add SWITCH platform for monitor power on/off - Add display API client methods (get_display_monitors, set_display_brightness, set_display_power) - Add display API constants Co-Authored-By: Claude Opus 4.6 --- .../remote_media_player/__init__.py | 2 +- .../remote_media_player/api_client.py | 39 ++++++ .../remote_media_player/const.py | 3 + .../remote_media_player/number.py | 110 +++++++++++++++ .../remote_media_player/switch.py | 125 ++++++++++++++++++ 5 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 custom_components/remote_media_player/number.py create mode 100644 custom_components/remote_media_player/switch.py diff --git a/custom_components/remote_media_player/__init__.py b/custom_components/remote_media_player/__init__.py index 3b4822c..a587bb9 100644 --- a/custom_components/remote_media_player/__init__.py +++ b/custom_components/remote_media_player/__init__.py @@ -27,7 +27,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.BUTTON] +PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.BUTTON, Platform.NUMBER, Platform.SWITCH] # Service schema for execute_script SERVICE_EXECUTE_SCRIPT_SCHEMA = vol.Schema( diff --git a/custom_components/remote_media_player/api_client.py b/custom_components/remote_media_player/api_client.py index 507c9de..5bbe0f7 100644 --- a/custom_components/remote_media_player/api_client.py +++ b/custom_components/remote_media_player/api_client.py @@ -30,6 +30,9 @@ from .const import ( API_BROWSER_FOLDERS, API_BROWSER_BROWSE, API_BROWSER_PLAY, + API_DISPLAY_MONITORS, + API_DISPLAY_BRIGHTNESS, + API_DISPLAY_POWER, ) _LOGGER = logging.getLogger(__name__) @@ -342,6 +345,42 @@ 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. + + Returns: + List of monitor dicts with id, name, brightness, power_supported, power_on, resolution + """ + return await self._request("GET", f"{API_DISPLAY_MONITORS}?refresh=true") + + async def set_display_brightness(self, monitor_id: int, brightness: int) -> dict[str, Any]: + """Set brightness for a specific monitor. + + Args: + monitor_id: Monitor index + brightness: Brightness level (0-100) + + Returns: + Response data with success status + """ + return await self._request( + "POST", f"{API_DISPLAY_BRIGHTNESS}/{monitor_id}", {"brightness": brightness} + ) + + async def set_display_power(self, monitor_id: int, on: bool) -> dict[str, Any]: + """Set power state for a specific monitor. + + Args: + monitor_id: Monitor index + on: True to turn on, False to turn off + + Returns: + Response data with success status + """ + return await self._request( + "POST", f"{API_DISPLAY_POWER}/{monitor_id}", {"on": on} + ) + class MediaServerWebSocket: """WebSocket client for real-time media status updates.""" diff --git a/custom_components/remote_media_player/const.py b/custom_components/remote_media_player/const.py index e903b74..485fb2f 100644 --- a/custom_components/remote_media_player/const.py +++ b/custom_components/remote_media_player/const.py @@ -37,6 +37,9 @@ API_WEBSOCKET = "/api/media/ws" API_BROWSER_FOLDERS = "/api/browser/folders" API_BROWSER_BROWSE = "/api/browser/browse" API_BROWSER_PLAY = "/api/browser/play" +API_DISPLAY_MONITORS = "/api/display/monitors" +API_DISPLAY_BRIGHTNESS = "/api/display/brightness" +API_DISPLAY_POWER = "/api/display/power" # Service names SERVICE_EXECUTE_SCRIPT = "execute_script" diff --git a/custom_components/remote_media_player/number.py b/custom_components/remote_media_player/number.py new file mode 100644 index 0000000..f636c73 --- /dev/null +++ b/custom_components/remote_media_player/number.py @@ -0,0 +1,110 @@ +"""Number platform for Remote Media Player integration (display brightness).""" + +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .api_client import MediaServerClient, MediaServerError +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up display brightness number entities from a config entry.""" + client: MediaServerClient = hass.data[DOMAIN][entry.entry_id]["client"] + + try: + monitors = await client.get_display_monitors() + except MediaServerError as err: + _LOGGER.error("Failed to fetch display monitors: %s", err) + return + + entities = [ + DisplayBrightnessNumber( + client=client, + entry=entry, + monitor=monitor, + ) + for monitor in monitors + if monitor.get("brightness") is not None + ] + + if entities: + async_add_entities(entities) + _LOGGER.info("Added %d display brightness entities", len(entities)) + + +class DisplayBrightnessNumber(NumberEntity): + """Number entity for controlling display brightness.""" + + _attr_has_entity_name = True + _attr_native_min_value = 0 + _attr_native_max_value = 100 + _attr_native_step = 1 + _attr_native_unit_of_measurement = "%" + _attr_mode = NumberMode.SLIDER + _attr_icon = "mdi:brightness-6" + + def __init__( + self, + client: MediaServerClient, + entry: ConfigEntry, + monitor: dict[str, Any], + ) -> None: + """Initialize the display brightness entity.""" + self._client = client + self._entry = entry + self._monitor_id: int = monitor["id"] + self._monitor_name: str = monitor.get("name", f"Monitor {monitor['id']}") + self._resolution: str | None = monitor.get("resolution") + self._attr_native_value = monitor.get("brightness") + + # Use resolution in name to disambiguate same-name monitors + display_name = self._monitor_name + if self._resolution: + display_name = f"{self._monitor_name} ({self._resolution})" + + self._attr_unique_id = f"{entry.entry_id}_display_brightness_{self._monitor_id}" + self._attr_name = f"Display {display_name} Brightness" + + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return DeviceInfo( + identifiers={(DOMAIN, self._entry.entry_id)}, + name=self._entry.title, + manufacturer="Remote Media Player", + model="Media Server", + ) + + async def async_set_native_value(self, value: float) -> None: + """Set the brightness value.""" + try: + await self._client.set_display_brightness(self._monitor_id, int(value)) + self._attr_native_value = int(value) + self.async_write_ha_state() + except MediaServerError as err: + _LOGGER.error("Failed to set brightness for monitor %d: %s", self._monitor_id, err) + + async def async_update(self) -> None: + """Fetch updated brightness from the server.""" + try: + monitors = await self._client.get_display_monitors() + for monitor in monitors: + if monitor["id"] == self._monitor_id: + self._attr_native_value = monitor.get("brightness") + break + except MediaServerError as err: + _LOGGER.error("Failed to update brightness for monitor %d: %s", self._monitor_id, err) diff --git a/custom_components/remote_media_player/switch.py b/custom_components/remote_media_player/switch.py new file mode 100644 index 0000000..75c8674 --- /dev/null +++ b/custom_components/remote_media_player/switch.py @@ -0,0 +1,125 @@ +"""Switch platform for Remote Media Player integration (display power).""" + +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .api_client import MediaServerClient, MediaServerError +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up display power switch entities from a config entry.""" + client: MediaServerClient = hass.data[DOMAIN][entry.entry_id]["client"] + + try: + monitors = await client.get_display_monitors() + except MediaServerError as err: + _LOGGER.error("Failed to fetch display monitors: %s", err) + return + + entities = [ + DisplayPowerSwitch( + client=client, + entry=entry, + monitor=monitor, + ) + for monitor in monitors + if monitor.get("power_supported", False) + ] + + if entities: + async_add_entities(entities) + _LOGGER.info("Added %d display power switch entities", len(entities)) + + +class DisplayPowerSwitch(SwitchEntity): + """Switch entity for controlling display power.""" + + _attr_has_entity_name = True + _attr_device_class = SwitchDeviceClass.SWITCH + + def __init__( + self, + client: MediaServerClient, + entry: ConfigEntry, + monitor: dict[str, Any], + ) -> None: + """Initialize the display power switch.""" + self._client = client + self._entry = entry + self._monitor_id: int = monitor["id"] + self._monitor_name: str = monitor.get("name", f"Monitor {monitor['id']}") + self._resolution: str | None = monitor.get("resolution") + self._attr_is_on = monitor.get("power_on", True) + + # Use resolution in name to disambiguate same-name monitors + display_name = self._monitor_name + if self._resolution: + display_name = f"{self._monitor_name} ({self._resolution})" + + self._attr_unique_id = f"{entry.entry_id}_display_power_{self._monitor_id}" + self._attr_name = f"Display {display_name} Power" + + @property + def icon(self) -> str: + """Return icon based on power state.""" + return "mdi:monitor" if self._attr_is_on else "mdi:monitor-off" + + @property + def device_info(self) -> DeviceInfo: + """Return device info.""" + return DeviceInfo( + identifiers={(DOMAIN, self._entry.entry_id)}, + name=self._entry.title, + manufacturer="Remote Media Player", + model="Media Server", + ) + + 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) + + 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)