From 1da7927fbb81e635908c8a720f912e74214df532 Mon Sep 17 00:00:00 2001 From: Bram Gerritsen Date: Tue, 19 Apr 2022 06:32:18 +0200 Subject: [PATCH] Fix issue with turning the ambilight on after switched off (#69132) * Refactor ambilight component. Fix issue with turning the ambilight on after switched off * Move dataclass to original location. Add factory method * Remove follow video effect list * Fix log * Remove follow video effect list * Update homeassistant/components/philips_js/light.py Co-authored-by: Joakim Plate * Update homeassistant/components/philips_js/light.py Co-authored-by: Joakim Plate * Add missing typing * Fix issues with restoring last state Co-authored-by: Joakim Plate --- homeassistant/components/philips_js/light.py | 185 +++++++++++-------- 1 file changed, 103 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 21bb7199269..72bfd71a876 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -1,6 +1,8 @@ """Component to integrate ambilight for TVs exposing the Joint Space API.""" from __future__ import annotations +from dataclasses import dataclass + from haphilipsjs import PhilipsTV from haphilipsjs.typing import AmbilightCurrentConfiguration @@ -51,44 +53,54 @@ def _get_settings(style: AmbilightCurrentConfiguration): return None -def _parse_effect(effect: str): - style, _, algorithm = effect.partition(EFFECT_PARTITION) - if style == EFFECT_MODE: - return EFFECT_MODE, algorithm, None - algorithm, _, expert = algorithm.partition(EFFECT_PARTITION) - if expert: - return EFFECT_EXPERT, style, algorithm - return EFFECT_AUTO, style, algorithm +@dataclass +class AmbilightEffect: + """Data class describing the ambilight effect.""" + mode: str + style: str + algorithm: str | None = None -def _get_effect(mode: str, style: str, algorithm: str | None): - if mode == EFFECT_MODE: - return f"{EFFECT_MODE}{EFFECT_PARTITION}{style}" - if mode == EFFECT_EXPERT: - return f"{style}{EFFECT_PARTITION}{algorithm}{EFFECT_PARTITION}{EFFECT_EXPERT}" - return f"{style}{EFFECT_PARTITION}{algorithm}" + def is_on(self, powerstate) -> bool: + """Check whether the ambilight is considered on.""" + if self.mode in (EFFECT_AUTO, EFFECT_EXPERT): + if self.style in ("FOLLOW_VIDEO", "FOLLOW_AUDIO"): + return powerstate in ("On", None) + if self.style == "OFF": + return False + return True + if self.mode == EFFECT_MODE: + if self.style == "internal": + return powerstate in ("On", None) + return True -def _is_on(mode, style, powerstate): - if mode in (EFFECT_AUTO, EFFECT_EXPERT): - if style in ("FOLLOW_VIDEO", "FOLLOW_AUDIO"): - return powerstate in ("On", None) - if style == "OFF": - return False + return False + + def is_valid(self) -> bool: + """Validate the effect configuration.""" + if self.mode == EFFECT_EXPERT: + return self.style in EFFECT_EXPERT_STYLES return True - if mode == EFFECT_MODE: - if style == "internal": - return powerstate in ("On", None) - return True + @staticmethod + def from_str(effect_string: str) -> AmbilightEffect: + """Create AmbilightEffect object from string.""" + style, _, algorithm = effect_string.partition(EFFECT_PARTITION) + if style == EFFECT_MODE: + return AmbilightEffect(mode=EFFECT_MODE, style=algorithm, algorithm=None) + algorithm, _, expert = algorithm.partition(EFFECT_PARTITION) + if expert: + return AmbilightEffect(mode=EFFECT_EXPERT, style=style, algorithm=algorithm) + return AmbilightEffect(mode=EFFECT_AUTO, style=style, algorithm=algorithm) - return False - - -def _is_valid(mode, style): - if mode == EFFECT_EXPERT: - return style in EFFECT_EXPERT_STYLES - return True + def __str__(self) -> str: + """Get a string representation of the effect.""" + if self.mode == EFFECT_MODE: + return f"{EFFECT_MODE}{EFFECT_PARTITION}{self.style}" + if self.mode == EFFECT_EXPERT: + return f"{self.style}{EFFECT_PARTITION}{self.algorithm}{EFFECT_PARTITION}{EFFECT_EXPERT}" + return f"{self.style}{EFFECT_PARTITION}{self.algorithm}" def _get_cache_keys(device: PhilipsTV): @@ -137,6 +149,7 @@ class PhilipsTVLightEntity( self._hs = None self._brightness = None self._cache_keys = None + self._last_selected_effect: AmbilightEffect = None super().__init__(coordinator) self._attr_supported_color_modes = [COLOR_MODE_HS, COLOR_MODE_ONOFF] @@ -160,48 +173,48 @@ class PhilipsTVLightEntity( def _calculate_effect_list(self): """Calculate an effect list based on current status.""" - effects = [] + effects: list[AmbilightEffect] = [] effects.extend( - _get_effect(EFFECT_AUTO, style, setting) + AmbilightEffect(mode=EFFECT_AUTO, style=style, algorithm=setting) for style, data in self._tv.ambilight_styles.items() - if _is_valid(EFFECT_AUTO, style) - and _is_on(EFFECT_AUTO, style, self._tv.powerstate) for setting in data.get("menuSettings", []) ) effects.extend( - _get_effect(EFFECT_EXPERT, style, algorithm) + AmbilightEffect(mode=EFFECT_EXPERT, style=style, algorithm=algorithm) for style, data in self._tv.ambilight_styles.items() - if _is_valid(EFFECT_EXPERT, style) - and _is_on(EFFECT_EXPERT, style, self._tv.powerstate) for algorithm in data.get("algorithms", []) ) effects.extend( - _get_effect(EFFECT_MODE, style, None) + AmbilightEffect(mode=EFFECT_MODE, style=style) for style in self._tv.ambilight_modes - if _is_valid(EFFECT_MODE, style) - and _is_on(EFFECT_MODE, style, self._tv.powerstate) ) - return sorted(effects) + filtered_effects = [ + str(effect) + for effect in effects + if effect.is_valid() and effect.is_on(self._tv.powerstate) + ] - def _calculate_effect(self): + return sorted(filtered_effects) + + def _calculate_effect(self) -> AmbilightEffect: """Return the current effect.""" current = self._tv.ambilight_current_configuration if current and self._tv.ambilight_mode != "manual": if current["isExpert"]: if settings := _get_settings(current): - return _get_effect( + return AmbilightEffect( EFFECT_EXPERT, current["styleName"], settings["algorithm"] ) - return _get_effect(EFFECT_EXPERT, current["styleName"], None) + return AmbilightEffect(EFFECT_EXPERT, current["styleName"], None) - return _get_effect( + return AmbilightEffect( EFFECT_AUTO, current["styleName"], current.get("menuSetting", None) ) - return _get_effect(EFFECT_MODE, self._tv.ambilight_mode, None) + return AmbilightEffect(EFFECT_MODE, self._tv.ambilight_mode, None) @property def color_mode(self): @@ -219,8 +232,8 @@ class PhilipsTVLightEntity( def is_on(self): """Return if the light is turned on.""" if self._tv.on: - mode, style, _ = _parse_effect(self.effect) - return _is_on(mode, style, self._tv.powerstate) + effect = AmbilightEffect.from_str(self.effect) + return effect.is_on(self._tv.powerstate) return False @@ -231,21 +244,23 @@ class PhilipsTVLightEntity( if (cache_keys := _get_cache_keys(self._tv)) != self._cache_keys: self._cache_keys = cache_keys self._attr_effect_list = self._calculate_effect_list() - self._attr_effect = self._calculate_effect() + self._attr_effect = str(self._calculate_effect()) if current and current["isExpert"]: if settings := _get_settings(current): color = settings["color"] - mode, _, _ = _parse_effect(self._attr_effect) + effect = AmbilightEffect.from_str(self._attr_effect) + if effect.is_on(self._tv.powerstate): + self._last_selected_effect = effect - if mode == EFFECT_EXPERT and color: + if effect.mode == EFFECT_EXPERT and color: self._attr_hs_color = ( color["hue"] * 360.0 / 255.0, color["saturation"] * 100.0 / 255.0, ) self._attr_brightness = color["brightness"] - elif mode == EFFECT_MODE and self._tv.ambilight_cached: + elif effect.mode == EFFECT_MODE and self._tv.ambilight_cached: hsv_h, hsv_s, hsv_v = color_RGB_to_hsv( *_average_pixels(self._tv.ambilight_cached) ) @@ -261,7 +276,9 @@ class PhilipsTVLightEntity( self._update_from_coordinator() super()._handle_coordinator_update() - async def _set_ambilight_cached(self, algorithm, hs_color, brightness): + async def _set_ambilight_cached( + self, effect: AmbilightEffect, hs_color: tuple[float, float], brightness: int + ): """Set ambilight via the manual or expert mode.""" rgb = color_hsv_to_RGB(hs_color[0], hs_color[1], brightness * 100 / 255) @@ -274,21 +291,21 @@ class PhilipsTVLightEntity( if not await self._tv.setAmbilightCached(data): raise Exception("Failed to set ambilight color") - if algorithm != self._tv.ambilight_mode: - if not await self._tv.setAmbilightMode(algorithm): + if effect.style != self._tv.ambilight_mode: + if not await self._tv.setAmbilightMode(effect.style): raise Exception("Failed to set ambilight mode") async def _set_ambilight_expert_config( - self, style, algorithm, hs_color, brightness + self, effect: AmbilightEffect, hs_color: tuple[float, float], brightness: int ): """Set ambilight via current configuration.""" config: AmbilightCurrentConfiguration = { - "styleName": style, + "styleName": effect.style, "isExpert": True, } setting = { - "algorithm": algorithm, + "algorithm": effect.algorithm, "color": { "hue": round(hs_color[0] * 255.0 / 360.0), "saturation": round(hs_color[1] * 255.0 / 100.0), @@ -301,23 +318,23 @@ class PhilipsTVLightEntity( }, } - if style in ("FOLLOW_COLOR", "Lounge light"): + if effect.style in ("FOLLOW_COLOR", "Lounge light"): config["colorSettings"] = setting config["speed"] = 2 - elif style == "FOLLOW_AUDIO": + elif effect.style == "FOLLOW_AUDIO": config["audioSettings"] = setting config["tuning"] = 0 if not await self._tv.setAmbilightCurrentConfiguration(config): raise Exception("Failed to set ambilight mode") - async def _set_ambilight_config(self, style, algorithm): + async def _set_ambilight_config(self, effect: AmbilightEffect): """Set ambilight via current configuration.""" config: AmbilightCurrentConfiguration = { - "styleName": style, + "styleName": effect.style, "isExpert": False, - "menuSetting": algorithm, + "menuSetting": effect.algorithm, } if await self._tv.setAmbilightCurrentConfiguration(config) is False: @@ -327,35 +344,39 @@ class PhilipsTVLightEntity( """Turn the bulb on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) - effect = kwargs.get(ATTR_EFFECT, self.effect) + attr_effect = kwargs.get(ATTR_EFFECT, self.effect) if not self._tv.on: raise Exception("TV is not available") - mode, style, setting = _parse_effect(effect) + effect = AmbilightEffect.from_str(attr_effect) - if not _is_on(mode, style, self._tv.powerstate): - mode = EFFECT_MODE - setting = None - if self._tv.powerstate in ("On", None): - style = "internal" + if effect.style == "OFF": + if self._last_selected_effect: + effect = self._last_selected_effect else: - style = "manual" + effect = AmbilightEffect(EFFECT_AUTO, "FOLLOW_VIDEO", "STANDARD") + + if not effect.is_on(self._tv.powerstate): + effect.mode = EFFECT_MODE + effect.algorithm = None + if self._tv.powerstate in ("On", None): + effect.style = "internal" + else: + effect.style = "manual" if brightness is None: brightness = 255 if hs_color is None: - hs_color = [0, 0] + hs_color = (0, 0) - if mode == EFFECT_MODE: - await self._set_ambilight_cached(style, hs_color, brightness) - elif mode == EFFECT_AUTO: - await self._set_ambilight_config(style, setting) - elif mode == EFFECT_EXPERT: - await self._set_ambilight_expert_config( - style, setting, hs_color, brightness - ) + if effect.mode == EFFECT_MODE: + await self._set_ambilight_cached(effect, hs_color, brightness) + elif effect.mode == EFFECT_AUTO: + await self._set_ambilight_config(effect) + elif effect.mode == EFFECT_EXPERT: + await self._set_ambilight_expert_config(effect, hs_color, brightness) self._update_from_coordinator() self.async_write_ha_state() @@ -369,7 +390,7 @@ class PhilipsTVLightEntity( if await self._tv.setAmbilightMode("internal") is False: raise Exception("Failed to set ambilight mode") - await self._set_ambilight_config("OFF", "") + await self._set_ambilight_config(AmbilightEffect(EFFECT_MODE, "OFF", "")) self._update_from_coordinator() self.async_write_ha_state()