From 7c6297db86bfa2916cf3bdce7d921596cd21cac8 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 4 Jan 2022 16:14:44 +0100 Subject: [PATCH] Add support for philips js screen state (#62775) --- .coveragerc | 1 + .../components/philips_js/__init__.py | 41 +++++++++-- homeassistant/components/philips_js/light.py | 26 ++----- .../components/philips_js/media_player.py | 46 ++++-------- homeassistant/components/philips_js/remote.py | 64 +++++------------ homeassistant/components/philips_js/switch.py | 72 +++++++++++++++++++ tests/components/philips_js/conftest.py | 4 +- 7 files changed, 149 insertions(+), 105 deletions(-) create mode 100644 homeassistant/components/philips_js/switch.py diff --git a/.coveragerc b/.coveragerc index d0c15a448f7..0fff4590505 100644 --- a/.coveragerc +++ b/.coveragerc @@ -827,6 +827,7 @@ omit = homeassistant/components/philips_js/light.py homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/remote.py + homeassistant/components/philips_js/switch.py homeassistant/components/pi_hole/sensor.py homeassistant/components/pi4ioe5v9xxxx/binary_sensor.py homeassistant/components/pi4ioe5v9xxxx/switch.py diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index fe3e46c70f1..1292310f134 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -2,12 +2,13 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Mapping from datetime import timedelta import logging from typing import Any from haphilipsjs import ConnectionFailure, PhilipsTV +from haphilipsjs.typing import SystemType from homeassistant.components.automation import AutomationActionType from homeassistant.config_entries import ConfigEntry @@ -18,13 +19,25 @@ from homeassistant.const import ( CONF_USERNAME, Platform, ) -from homeassistant.core import CALLBACK_TYPE, Context, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Context, + Event, + HassJob, + HomeAssistant, + callback, +) from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_ALLOW_NOTIFY, DOMAIN +from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN -PLATFORMS = [Platform.MEDIA_PLAYER, Platform.LIGHT, Platform.REMOTE] +PLATFORMS = [ + Platform.MEDIA_PLAYER, + Platform.LIGHT, + Platform.REMOTE, + Platform.SWITCH, +] LOGGER = logging.getLogger(__name__) @@ -71,7 +84,7 @@ class PluggableAction: def __init__(self, update: Callable[[], None]) -> None: """Initialize.""" self._update = update - self._actions: dict[Any, AutomationActionType] = {} + self._actions: dict[Any, tuple[HassJob, dict[str, Any]]] = {} def __bool__(self): """Return if we have something attached.""" @@ -102,7 +115,7 @@ class PluggableAction: class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): """Coordinator to update data.""" - def __init__(self, hass, api: PhilipsTV, options: dict) -> None: + def __init__(self, hass, api: PhilipsTV, options: Mapping) -> None: """Set up the coordinator.""" self.api = api self.options = options @@ -125,6 +138,20 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): ), ) + @property + def system(self) -> SystemType: + """Return the system descriptor.""" + if self.api.system: + return self.api.system + return self.config_entry.data[CONF_SYSTEM] + + @property + def unique_id(self) -> str: + """Return the system descriptor.""" + assert self.config_entry + assert self.config_entry.unique_id + return self.config_entry.unique_id + @property def _notify_wanted(self): """Return if the notify feature should be active. @@ -170,7 +197,7 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): self._async_notify_stop() @callback - def _async_stop_refresh(self, event: asyncio.Event) -> None: + def _async_stop_refresh(self, event: Event) -> None: super()._async_stop_refresh(event) self._async_notify_stop() diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 93b65db90fa..ef5333a329d 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -1,8 +1,6 @@ """Component to integrate ambilight for TVs exposing the Joint Space API.""" from __future__ import annotations -from typing import Any - from haphilipsjs import PhilipsTV from haphilipsjs.typing import AmbilightCurrentConfiguration @@ -24,7 +22,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.color import color_hsv_to_RGB, color_RGB_to_hsv from . import PhilipsTVDataUpdateCoordinator -from .const import CONF_SYSTEM, DOMAIN +from .const import DOMAIN EFFECT_PARTITION = ": " EFFECT_MODE = "Mode" @@ -40,13 +38,7 @@ async def async_setup_entry( ): """Set up the configuration entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - [ - PhilipsTVLightEntity( - coordinator, config_entry.data[CONF_SYSTEM], config_entry.unique_id - ) - ] - ) + async_add_entities([PhilipsTVLightEntity(coordinator)]) def _get_settings(style: AmbilightCurrentConfiguration): @@ -136,15 +128,11 @@ class PhilipsTVLightEntity(CoordinatorEntity, LightEntity): def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, - system: dict[str, Any], - unique_id: str, ) -> None: """Initialize light.""" self._tv = coordinator.api self._hs = None self._brightness = None - self._system = system - self._coordinator = coordinator self._cache_keys = None super().__init__(coordinator) @@ -152,17 +140,17 @@ class PhilipsTVLightEntity(CoordinatorEntity, LightEntity): self._attr_supported_features = ( SUPPORT_EFFECT | SUPPORT_COLOR | SUPPORT_BRIGHTNESS ) - self._attr_name = self._system["name"] - self._attr_unique_id = unique_id + self._attr_name = f"{coordinator.system['name']} Ambilight" + self._attr_unique_id = coordinator.unique_id self._attr_icon = "mdi:television-ambient-light" self._attr_device_info = DeviceInfo( identifiers={ (DOMAIN, self._attr_unique_id), }, manufacturer="Philips", - model=self._system.get("model"), - name=self._system["name"], - sw_version=self._system.get("softwareversion"), + model=coordinator.system.get("model"), + name=coordinator.system["name"], + sw_version=coordinator.system.get("softwareversion"), ) self._update_from_coordinator() diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index fa5643503bc..53aa20f6606 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -1,8 +1,6 @@ """Media Player component to integrate TVs exposing the Joint Space API.""" from __future__ import annotations -from typing import Any - from haphilipsjs import ConnectionFailure from homeassistant import config_entries @@ -40,7 +38,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER as _LOGGER, PhilipsTVDataUpdateCoordinator -from .const import CONF_SYSTEM, DOMAIN +from .const import DOMAIN SUPPORT_PHILIPS_JS = ( SUPPORT_TURN_OFF @@ -75,8 +73,6 @@ async def async_setup_entry( [ PhilipsTVMediaPlayer( coordinator, - config_entry.data[CONF_SYSTEM], - config_entry.unique_id, ) ] ) @@ -85,13 +81,12 @@ async def async_setup_entry( class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): """Representation of a Philips TV exposing the JointSpace API.""" + _coordinator: PhilipsTVDataUpdateCoordinator _attr_device_class = MediaPlayerDeviceClass.TV def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, - system: dict[str, Any], - unique_id: str, ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api @@ -99,8 +94,18 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): self._sources = {} self._channels = {} self._supports = SUPPORT_PHILIPS_JS - self._system = system - self._unique_id = unique_id + self._system = coordinator.system + self._attr_name = coordinator.system["name"] + self._attr_unique_id = coordinator.unique_id + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, coordinator.unique_id), + }, + manufacturer="Philips", + model=coordinator.system.get("model"), + sw_version=coordinator.system.get("softwareversion"), + name=coordinator.system["name"], + ) self._state = STATE_OFF self._media_content_type: str | None = None self._media_content_id: str | None = None @@ -115,11 +120,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): self.async_write_ha_state() await self.coordinator.async_request_refresh() - @property - def name(self): - """Return the device name.""" - return self._system["name"] - @property def supported_features(self): """Flag media player features that are supported.""" @@ -280,24 +280,6 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): if app := self._tv.applications.get(self._tv.application_id): return app.get("label") - @property - def unique_id(self): - """Return unique identifier if known.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return a device description for device registry.""" - return DeviceInfo( - identifiers={ - (DOMAIN, self._unique_id), - }, - manufacturer="Philips", - model=self._system.get("model"), - sw_version=self._system.get("softwareversion"), - name=self._system["name"], - ) - async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index de1e2ce43a3..6bf60f7f5b0 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -1,8 +1,6 @@ """Remote control support for Apple TV.""" import asyncio -from haphilipsjs.typing import SystemType - from homeassistant.components.remote import ( ATTR_DELAY_SECS, ATTR_NUM_REPEATS, @@ -13,9 +11,10 @@ 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 homeassistant.helpers.update_coordinator import CoordinatorEntity from . import LOGGER, PhilipsTVDataUpdateCoordinator -from .const import CONF_SYSTEM, DOMAIN +from .const import DOMAIN async def async_setup_entry( @@ -25,36 +24,32 @@ async def async_setup_entry( ) -> None: """Set up the configuration entry.""" coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - [ - PhilipsTVRemote( - coordinator, - config_entry.data[CONF_SYSTEM], - config_entry.unique_id, - ) - ] - ) + async_add_entities([PhilipsTVRemote(coordinator)]) -class PhilipsTVRemote(RemoteEntity): +class PhilipsTVRemote(CoordinatorEntity, RemoteEntity): """Device that sends commands.""" + _coordinator: PhilipsTVDataUpdateCoordinator + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, - system: SystemType, - unique_id: str, ) -> None: """Initialize the Philips TV.""" + super().__init__(coordinator) self._tv = coordinator.api - self._coordinator = coordinator - self._system = system - self._unique_id = unique_id - - @property - def name(self): - """Return the device name.""" - return self._system["name"] + self._attr_name = f"{coordinator.system['name']} Remote" + self._attr_unique_id = coordinator.unique_id + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, coordinator.unique_id), + }, + manufacturer="Philips", + model=coordinator.system.get("model"), + name=coordinator.system["name"], + sw_version=coordinator.system.get("softwareversion"), + ) @property def is_on(self): @@ -63,29 +58,6 @@ class PhilipsTVRemote(RemoteEntity): self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None) ) - @property - def should_poll(self): - """No polling needed for Apple TV.""" - return False - - @property - def unique_id(self): - """Return unique identifier if known.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return a device description for device registry.""" - return DeviceInfo( - identifiers={ - (DOMAIN, self._unique_id), - }, - manufacturer="Philips", - model=self._system.get("model"), - name=self._system["name"], - sw_version=self._system.get("softwareversion"), - ) - async def async_turn_on(self, **kwargs): """Turn the device on.""" if self._tv.on and self._tv.powerstate: diff --git a/homeassistant/components/philips_js/switch.py b/homeassistant/components/philips_js/switch.py new file mode 100644 index 00000000000..a565ea67ad6 --- /dev/null +++ b/homeassistant/components/philips_js/switch.py @@ -0,0 +1,72 @@ +"""Philips TV menu switches.""" +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.components.switch import SwitchEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import PhilipsTVDataUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +): + """Set up the configuration entry.""" + coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities([PhilipsTVScreenSwitch(coordinator)]) + + +class PhilipsTVScreenSwitch(CoordinatorEntity, SwitchEntity): + """A Philips TV screen state switch.""" + + coordinator: PhilipsTVDataUpdateCoordinator + + def __init__( + self, + coordinator: PhilipsTVDataUpdateCoordinator, + ) -> None: + """Initialize entity.""" + + super().__init__(coordinator) + + self._attr_name = f"{coordinator.system['name']} Screen State" + self._attr_icon = "mdi:television-shimmer" + self._attr_unique_id = f"{coordinator.unique_id}_screenstate" + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, coordinator.unique_id), + } + ) + + @property + def available(self) -> bool: + """Return true if entity is available.""" + if not super().available: + return False + if not self.coordinator.api.on: + return False + return self.coordinator.api.powerstate == "On" + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self.coordinator.api.screenstate == "On" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.coordinator.api.setScreenState("On") + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.coordinator.api.setScreenState("Off") diff --git a/tests/components/philips_js/conftest.py b/tests/components/philips_js/conftest.py index fc7e142bf53..e0069cf9b75 100644 --- a/tests/components/philips_js/conftest.py +++ b/tests/components/philips_js/conftest.py @@ -41,7 +41,9 @@ def mock_tv(): @fixture async def mock_config_entry(hass): """Get standard player.""" - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME) + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME, unique_id="ABCDEFGHIJKLF" + ) config_entry.add_to_hass(hass) return config_entry