Skip to content

Commit

Permalink
Support color profile for temperature
Browse files Browse the repository at this point in the history
  • Loading branch information
dext0r committed Jun 16, 2022
1 parent 637753f commit c49d392
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 13 deletions.
65 changes: 55 additions & 10 deletions custom_components/yandex_smart_home/capability_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ class ColorSettingCapability(AbstractCapability, ABC):
default_white_temperature_k = 4500
cold_white_temperature_k = 6500

def __init__(self, hass: HomeAssistant, config: Config, state: State):
super().__init__(hass, config, state)

if self.state.domain == light.DOMAIN:
self._color_profiles = COLOR_PROFILES.copy()
self._color_profiles.update(config.color_profiles)

def parameters(self) -> dict[str, Any]:
"""Return parameters for a devices request."""
result = {}
Expand Down Expand Up @@ -184,20 +191,28 @@ def get_ha_effect_by_yandex_scene(self, yandex_scene: str) -> str | None:
if str(am) == ha_effect:
return ha_effect

@property
def _temperature_converter(self) -> TemperatureConverter:
color_profile_name = self.entity_config.get(const.CONF_COLOR_PROFILE)
if color_profile_name:
try:
return TemperatureConverter(self._color_profiles[color_profile_name])
except KeyError:
raise SmartHomeError(
ERR_NOT_SUPPORTED_IN_CURRENT_MODE,
f'Color profile {color_profile_name!r} not found for instance {self.instance} '
f'of {self.state.entity_id}'
)

return TemperatureConverter()


@register_capability
class RgbCapability(ColorSettingCapability):
"""RGB color functionality."""

instance = const.COLOR_SETTING_RGB

def __init__(self, hass: HomeAssistant, config: Config, state: State):
super().__init__(hass, config, state)

if self.state.domain == light.DOMAIN:
self._color_profiles = COLOR_PROFILES.copy()
self._color_profiles.update(config.color_profiles)

def supported(self) -> bool:
"""Test if capability is supported."""
return self.support_color
Expand Down Expand Up @@ -271,7 +286,7 @@ def get_value(self) -> float | None:
color_mode = self.state.attributes.get(light.ATTR_COLOR_MODE)

if temperature_mired is not None:
return color_temperature_mired_to_kelvin(temperature_mired)
return self._temperature_converter.get_yandex_temperature(temperature_mired)

if color_mode == light.ColorMode.WHITE:
return self.default_white_temperature_k
Expand Down Expand Up @@ -305,7 +320,7 @@ async def set_state(self, data: RequestData, state: dict[str, Any]):

if features & light.SUPPORT_COLOR_TEMP or \
light.color_temp_supported(self.state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, [])):
service_data[light.ATTR_KELVIN] = value
service_data[light.ATTR_KELVIN] = self._temperature_converter.get_ha_temperature(value)

elif light.ColorMode.WHITE in supported_color_modes and value == self.default_white_temperature_k:
service_data[light.ATTR_WHITE] = self.state.attributes.get(light.ATTR_BRIGHTNESS, 255)
Expand Down Expand Up @@ -380,11 +395,17 @@ def __init__(self, profile: dict[str, int] | None = None):
self._ha_value_to_yandex: dict[int, int] = {}

for name, yandex_value in self._palette.items():
ha_value = profile.get(name, yandex_value)
ha_value = self._normalize_ha_value(
profile.get(name, yandex_value)
)

self._yandex_value_to_ha[yandex_value] = ha_value
self._ha_value_to_yandex[ha_value] = yandex_value

@staticmethod
def _normalize_ha_value(v: int) -> int:
return v


class ColorConverter(ValueConverter):
_palette = {
Expand Down Expand Up @@ -442,3 +463,27 @@ def _distance(a: RGBColor, b: RGBColor) -> float:
(a.g - b.g) ** 2 +
(a.b - b.b) ** 2
))


class TemperatureConverter(ValueConverter):
_palette = {
const.COLOR_TEMPERATURE_NAME_FIERY_WHITE: 1500,
const.COLOR_TEMPERATURE_NAME_SOFT_WHITE: 2700,
const.COLOR_TEMPERATURE_NAME_WARM_WHITE: 3400,
const.COLOR_TEMPERATURE_NAME_WHITE: 4500,
const.COLOR_TEMPERATURE_NAME_DAYLIGHT: 5600,
const.COLOR_TEMPERATURE_NAME_COLD_WHITE: 6500,
const.COLOR_TEMPERATURE_NAME_MISTY_WHITE: 7500,
const.COLOR_TEMPERATURE_NAME_HEAVENLY_WHITE: 9000
}

def get_ha_temperature(self, yandex_temperature: int) -> int:
return self._yandex_value_to_ha.get(yandex_temperature, yandex_temperature)

def get_yandex_temperature(self, ha_temperature_mired: int) -> int:
ha_temperature_k = self._normalize_ha_value(color_temperature_mired_to_kelvin(ha_temperature_mired))
return self._ha_value_to_yandex.get(ha_temperature_k, ha_temperature_k)

@staticmethod
def _normalize_ha_value(v: int) -> int:
return round(v, -2)
18 changes: 17 additions & 1 deletion custom_components/yandex_smart_home/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,14 @@
COLOR_NAME_ORCHID = 'orchid'
COLOR_NAME_MAUVE = 'mauve'
COLOR_NAME_RASPBERRY = 'raspberry'
COLOR_TEMPERATURE_NAME_FIERY_WHITE = 'fiery_white'
COLOR_TEMPERATURE_NAME_SOFT_WHITE = 'soft_white'
COLOR_TEMPERATURE_NAME_WARM_WHITE = 'warm_white'
COLOR_TEMPERATURE_NAME_WHITE = 'white'
COLOR_TEMPERATURE_NAME_DAYLIGHT = 'daylight'
COLOR_TEMPERATURE_NAME_COLD_WHITE = 'cold_white'
COLOR_TEMPERATURE_NAME_MISTY_WHITE = 'misty_white'
COLOR_TEMPERATURE_NAME_HEAVENLY_WHITE = 'heavenly_white'
COLOR_NAMES = (
COLOR_NAME_RED,
COLOR_NAME_CORAL,
Expand All @@ -323,7 +331,15 @@
COLOR_NAME_PURPLE,
COLOR_NAME_ORCHID,
COLOR_NAME_MAUVE,
COLOR_NAME_RASPBERRY
COLOR_NAME_RASPBERRY,
COLOR_TEMPERATURE_NAME_FIERY_WHITE,
COLOR_TEMPERATURE_NAME_SOFT_WHITE,
COLOR_TEMPERATURE_NAME_WARM_WHITE,
COLOR_TEMPERATURE_NAME_WHITE,
COLOR_TEMPERATURE_NAME_DAYLIGHT,
COLOR_TEMPERATURE_NAME_COLD_WHITE,
COLOR_TEMPERATURE_NAME_MISTY_WHITE,
COLOR_TEMPERATURE_NAME_HEAVENLY_WHITE
)

# https://yandex.ru/dev/dialogs/smart-home/doc/concepts/mode-instance-modes.html
Expand Down
76 changes: 74 additions & 2 deletions tests/test_capability_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class ColorProfileMockConfig(MockConfig):
def color_profiles(self) -> dict[str, dict[str, int]]:
return {
'test': {
'red': ColorConverter.rgb_to_int(255, 191, 0)
'red': ColorConverter.rgb_to_int(255, 191, 0),
'white': 4120
}
}

Expand Down Expand Up @@ -203,7 +204,7 @@ async def test_capability_color_setting_temperature_k(hass, attributes, temp_ran

state = State('light.test', STATE_OFF, dict({light.ATTR_COLOR_TEMP: 370}, **attributes))
cap = get_exact_one_capability(hass, BASIC_CONFIG, state, CAPABILITIES_COLOR_SETTING, COLOR_SETTING_TEMPERATURE_K)
assert cap.get_value() == 2702
assert cap.get_value() == 2700

calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await cap.set_state(BASIC_DATA, {'value': 6500})
Expand All @@ -220,6 +221,77 @@ async def test_capability_color_setting_temperature_k(hass, attributes, temp_ran
assert e.value.code == const.ERR_NOT_SUPPORTED_IN_CURRENT_MODE


@pytest.mark.parametrize('attributes', [
{ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR_TEMP},
{light.ATTR_SUPPORTED_COLOR_MODES: [light.ColorMode.COLOR_TEMP]},
{light.ATTR_SUPPORTED_COLOR_MODES: [light.ColorMode.COLOR_TEMP, light.ColorMode.RGB]},
{light.ATTR_SUPPORTED_COLOR_MODES: [light.ColorMode.COLOR_TEMP, light.ColorMode.HS]},
])
async def test_capability_color_setting_temperature_k_with_profile(hass, attributes):
config = ColorProfileMockConfig(
entity_config={
'light.test': {
const.CONF_COLOR_PROFILE: 'test'
},
'light.invalid': {
const.CONF_COLOR_PROFILE: 'invalid'
}
},
entity_filter=generate_entity_filter(include_entity_globs=['*'])
)
attributes.update({
light.ATTR_MAX_MIREDS: 500,
light.ATTR_MIN_MIREDS: 200,
})

state = State('light.test', STATE_OFF, attributes)
cap = get_exact_one_capability(hass, config, state, CAPABILITIES_COLOR_SETTING, COLOR_SETTING_TEMPERATURE_K)
assert cap.retrievable
assert cap.parameters()['temperature_k'] == {
'max': 5000,
'min': 2000
}
assert cap.get_value() is None

state = State('light.test', STATE_OFF, dict({light.ATTR_COLOR_TEMP: 370}, **attributes))
cap = get_exact_one_capability(hass, config, state, CAPABILITIES_COLOR_SETTING, COLOR_SETTING_TEMPERATURE_K)
assert cap.get_value() == 2700

state = State('light.test', STATE_OFF, dict({light.ATTR_COLOR_TEMP: 238}, **attributes))
cap = get_exact_one_capability(hass, config, state, CAPABILITIES_COLOR_SETTING, COLOR_SETTING_TEMPERATURE_K)
assert cap.get_value() == 4200

state = State('light.test', STATE_OFF, dict({light.ATTR_COLOR_TEMP: 243}, **attributes)) # k: 4150
cap = get_exact_one_capability(hass, config, state, CAPABILITIES_COLOR_SETTING, COLOR_SETTING_TEMPERATURE_K)
assert cap.get_value() == 4500

calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await cap.set_state(BASIC_DATA, {'value': 4500})
assert len(calls) == 1
assert calls[0].data == {ATTR_ENTITY_ID: state.entity_id, light.ATTR_KELVIN: 4100}

state = State('light.test', STATE_OFF, dict({light.ATTR_COLOR_TEMP: 243}, **attributes)) # k: 4150
cap = get_exact_one_capability(hass, BASIC_CONFIG, state, CAPABILITIES_COLOR_SETTING, COLOR_SETTING_TEMPERATURE_K)
assert cap.get_value() == 4100

calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
await cap.set_state(BASIC_DATA, {'value': 4100})
assert len(calls) == 1
assert calls[0].data == {ATTR_ENTITY_ID: state.entity_id, light.ATTR_KELVIN: 4100}

state = State('light.invalid', STATE_OFF, dict({light.ATTR_COLOR_TEMP: 243}, **attributes))
cap = get_exact_one_capability(hass, config, state, CAPABILITIES_COLOR_SETTING, COLOR_SETTING_TEMPERATURE_K)
with pytest.raises(SmartHomeError) as e:
cap.get_value()
assert e.value.code == const.ERR_NOT_SUPPORTED_IN_CURRENT_MODE
assert e.value.message.startswith('Color profile')

with pytest.raises(SmartHomeError) as e:
await cap.set_state(BASIC_DATA, {'value': 4100})
assert e.value.code == const.ERR_NOT_SUPPORTED_IN_CURRENT_MODE
assert e.value.message.startswith('Color profile')


@pytest.mark.parametrize('color_modes', [
[light.ColorMode.RGB], [light.ColorMode.HS],
])
Expand Down

0 comments on commit c49d392

Please sign in to comment.