From 981121af436d4710dbb41035054d6e3133af48e8 Mon Sep 17 00:00:00 2001 From: Bram Gerritsen Date: Sun, 19 May 2024 09:05:12 +0200 Subject: [PATCH] Fallback to hs.csv when mode is color_temp and color_temp.csv is not available (#2249) * fix: fallback to hs.csv when mode is color_temp and color_temp.csv is missing * fix: fallback to hs.csv when mode is color_temp and color_temp.csv is missing --- custom_components/powercalc/strategy/lut.py | 37 ++++++++++++++++++--- tests/strategy/test_lut.py | 29 ++++++++++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/custom_components/powercalc/strategy/lut.py b/custom_components/powercalc/strategy/lut.py index 0f9713ff7..1f5c30d37 100644 --- a/custom_components/powercalc/strategy/lut.py +++ b/custom_components/powercalc/strategy/lut.py @@ -4,11 +4,13 @@ import logging import os from collections import defaultdict +from collections.abc import Mapping from csv import reader from dataclasses import dataclass from decimal import Decimal from functools import partial from gzip import GzipFile +from typing import Any import numpy as np from homeassistant.components import light @@ -21,6 +23,7 @@ ColorMode, ) from homeassistant.core import State +from homeassistant.util.color import color_temperature_to_hs from custom_components.powercalc.common import SourceEntity from custom_components.powercalc.errors import ( @@ -44,6 +47,7 @@ class LutRegistry: def __init__(self) -> None: self._lookup_dictionaries: dict[str, dict] = {} + self._supported_color_modes: dict[str, set[ColorMode]] = {} async def get_lookup_dictionary( self, @@ -90,6 +94,20 @@ def get_lut_file(power_profile: PowerProfile, color_mode: ColorMode) -> GzipFile raise LutFileNotFoundError("Data file not found: %s") + def get_supported_color_modes(self, power_profile: PowerProfile) -> set[ColorMode]: + """Return the color modes supported by the Profile.""" + cache_key = f"{power_profile.manufacturer}_{power_profile.model}_supported_color_modes" + supported_color_modes = self._supported_color_modes.get(cache_key) + if supported_color_modes is None: + supported_color_modes = set() + for file in os.listdir(power_profile.get_model_directory()): + if file.endswith(".csv.gz"): + color_mode = ColorMode(file.removesuffix(".csv.gz")) + if color_mode in LUT_COLOR_MODES: + supported_color_modes.add(color_mode) + self._supported_color_modes[cache_key] = supported_color_modes + return supported_color_modes + class LutStrategy(PowerCalculationStrategyInterface): def __init__( @@ -101,13 +119,13 @@ def __init__( self._source_entity = source_entity self._lut_registry = lut_registry self._profile = profile + self._strategy_color_modes: set[ColorMode] | None = None async def calculate(self, entity_state: State) -> Decimal | None: """Calculate the power consumption based on brightness, mired, hsl values.""" attrs = entity_state.attributes - color_mode = attrs.get(ATTR_COLOR_MODE) - if color_mode in COLOR_MODES_COLOR: - color_mode = ColorMode.HS + original_color_mode = attrs.get(ATTR_COLOR_MODE) + color_mode = await self.get_selected_color_mode(attrs) brightness = attrs.get(ATTR_BRIGHTNESS) if brightness is None: @@ -142,7 +160,7 @@ async def calculate(self, entity_state: State) -> Decimal | None: light_setting = LightSetting(color_mode=color_mode, brightness=brightness) if color_mode == ColorMode.HS: - hs = attrs[ATTR_HS_COLOR] + hs = color_temperature_to_hs(attrs[ATTR_COLOR_TEMP]) if original_color_mode == ColorMode.COLOR_TEMP else attrs[ATTR_HS_COLOR] light_setting.hue = int(hs[0] / 360 * 65535) light_setting.saturation = int(hs[1] / 100 * 255) _LOGGER.debug( @@ -171,6 +189,17 @@ async def calculate(self, entity_state: State) -> Decimal | None: _LOGGER.debug("%s: Calculated power:%s", entity_state.entity_id, power) return power + async def get_selected_color_mode(self, attrs: Mapping[str, Any]) -> ColorMode: + """Get the selected color mode for the entity.""" + color_mode = ColorMode(str(attrs.get(ATTR_COLOR_MODE))) + if color_mode in COLOR_MODES_COLOR: + color_mode = ColorMode.HS + profile_color_modes = self._lut_registry.get_supported_color_modes(self._profile) + if color_mode not in profile_color_modes and color_mode == ColorMode.COLOR_TEMP: + _LOGGER.debug("Color mode not natively supported, falling back to HS") + color_mode = ColorMode.HS + return color_mode + def lookup_power( self, lookup_table: LookupDictType, diff --git a/tests/strategy/test_lut.py b/tests/strategy/test_lut.py index d9c6235d7..c2480b35b 100644 --- a/tests/strategy/test_lut.py +++ b/tests/strategy/test_lut.py @@ -22,8 +22,7 @@ PowerCalculationStrategyInterface, ) from tests.common import run_powercalc_setup - -from .common import create_source_entity +from tests.strategy.common import create_source_entity async def test_colortemp_lut(hass: HomeAssistant) -> None: @@ -203,6 +202,32 @@ async def test_sensor_unavailable_for_unsupported_color_mode(hass: HomeAssistant assert "Lookup table not found for color mode" in caplog.text +async def test_fallback_color_temp_to_hs(hass: HomeAssistant) -> None: + """ + Test fallback is done when no color_temp.csv is available, but a hs.csv is. + Fixes issue where HUE bridge is falsly reporting color_temp as color_mode. + See: https://github.com/bramstroker/homeassistant-powercalc/issues/2247 + """ + + await run_powercalc_setup( + hass, + { + CONF_ENTITY_ID: "light.test", + CONF_MANUFACTURER: "signify", + CONF_MODEL: "LLC011", + }, + ) + + hass.states.async_set( + "light.test", + STATE_ON, + {ATTR_COLOR_MODE: ColorMode.COLOR_TEMP, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP: 500}, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.test_power").state == "1.42" + + async def _create_lut_strategy( hass: HomeAssistant, manufacturer: str,