From c552702d3617d55c7a0ef1eff79e1a79be0352fd Mon Sep 17 00:00:00 2001 From: Michael Kirsch Date: Sat, 14 Nov 2020 16:51:41 +0100 Subject: [PATCH 1/4] add accent colors --- custom_components/adaptive_lighting/const.py | 19 +++ custom_components/adaptive_lighting/switch.py | 151 +++++++++++++++++- 2 files changed, 164 insertions(+), 6 deletions(-) diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index e1f2e3cb..03f3bf0e 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -36,20 +36,34 @@ CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0 CONF_SUNSET_TIME = "sunset_time" CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45 +CONF_ACCENT_COLOR, DEFAULT_ACCENT_COLOR = "accent_color", None +CONF_ACCENT_COLOR_RGB, DEFAULT_ACCENT_COLOR_RGB = "accent_color_rgb", None +CONF_ACCENT_COLOR_XY, DEFAULT_ACCENT_COLOR_XY = "accent_color_xy", None +CONF_ACCENT_COLOR_HS, DEFAULT_ACCENT_COLOR_HS = "accent_color_hs", None +CONF_ACCENT_COLOR_MIX_MIN, DEFAULT_ACCENT_COLOR_MIX_MIN = "accent_color_mix_min", -50 +CONF_ACCENT_COLOR_MIX_MAX, DEFAULT_ACCENT_COLOR_MIX_MAX = "accent_color_mix_max", 100 + +ATTR_SWITCH_ACCENT_COLOR = "accent_color" SLEEP_MODE_SWITCH = "sleep_mode_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch" ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch" +ENABLE_ACCENT_COLOR_SWITCH = "enable_accent_color_switch" ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener" UNDO_UPDATE_LISTENER = "undo_update_listener" NONE_STR = "None" ATTR_ADAPT_COLOR = "adapt_color" ATTR_ADAPT_BRIGHTNESS = "adapt_brightness" +ATTR_USE_ACCENT_COLOR = "use_accent_color" SERVICE_SET_MANUAL_CONTROL = "set_manual_control" CONF_MANUAL_CONTROL = "manual_control" SERVICE_APPLY = "apply" CONF_TURN_ON_LIGHTS = "turn_on_lights" +SERVICE_SET_ACCENT_COLOR = "set_accent_color" +CONF_ACCENT_RGB = "rgb" +CONF_ACCENT_HS = "hs" +CONF_ACCENT_XY = "xy" TURNING_OFF_DELAY = 5 @@ -69,6 +83,11 @@ VALIDATION_TUPLES = [ (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), (CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)), (CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)), + (CONF_ACCENT_COLOR_RGB, DEFAULT_ACCENT_COLOR_RGB, vol.Any(None,[int])), + (CONF_ACCENT_COLOR_XY, DEFAULT_ACCENT_COLOR_XY, vol.Any(None,[float])), + (CONF_ACCENT_COLOR_HS, DEFAULT_ACCENT_COLOR_HS, vol.Any(None,[float])), + (CONF_ACCENT_COLOR_MIX_MIN, DEFAULT_ACCENT_COLOR_MIX_MIN, int_between(-100, 100)), + (CONF_ACCENT_COLOR_MIX_MAX, DEFAULT_ACCENT_COLOR_MIX_MAX, int_between(-100, 100)), (CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)), (CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP, int_between(1000, 10000)), (CONF_SUNRISE_TIME, NONE_STR, str), diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index bb1c5cda..32202ab1 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -76,10 +76,14 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_location from homeassistant.util import slugify from homeassistant.util.color import ( + color_hs_to_RGB, + color_hsv_to_RGB, + color_RGB_to_hsv, color_RGB_to_xy, color_temperature_kelvin_to_mired, color_temperature_to_rgb, color_xy_to_hs, + color_xy_to_RGB, ) import homeassistant.util.dt as dt_util @@ -89,6 +93,17 @@ from .const import ( ATTR_ADAPT_BRIGHTNESS, ATTR_ADAPT_COLOR, ATTR_TURN_ON_OFF_LISTENER, + ATTR_USE_ACCENT_COLOR, + ATTR_SWITCH_ACCENT_COLOR, + CONF_ACCENT_COLOR, + CONF_ACCENT_COLOR_HS, + CONF_ACCENT_COLOR_MIX_MAX, + CONF_ACCENT_COLOR_MIX_MIN, + CONF_ACCENT_COLOR_RGB, + CONF_ACCENT_COLOR_XY, + CONF_ACCENT_HS, + CONF_ACCENT_RGB, + CONF_ACCENT_XY, CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, CONF_INTERVAL, @@ -112,8 +127,10 @@ from .const import ( CONF_TURN_ON_LIGHTS, DOMAIN, EXTRA_VALIDATION, + ENABLE_ACCENT_COLOR_SWITCH, ICON, SERVICE_APPLY, + SERVICE_SET_ACCENT_COLOR, SERVICE_SET_MANUAL_CONTROL, SLEEP_MODE_SWITCH, SUN_EVENT_MIDNIGHT, @@ -220,10 +237,26 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall): data[CONF_TRANSITION], data[ATTR_ADAPT_BRIGHTNESS], data[ATTR_ADAPT_COLOR], + data[ATTR_USE_ACCENT_COLOR], data[CONF_PREFER_RGB_COLOR], force=True, ) +async def handle_set_accent_color(switch: AdaptiveSwitch, service_call: ServiceCall): + _LOGGER.debug( + "Called 'adaptive_lighting.set_accent_color' service with '%s'", + service_call.data, + ) + data = service_call.data + switch._sun_light_settings.accent_color = get_rgb_from_any_form( + data[CONF_ACCENT_RGB] if CONF_ACCENT_RGB in data else None, + data[CONF_ACCENT_HS] if CONF_ACCENT_HS in data else None, + data[CONF_ACCENT_XY] if CONF_ACCENT_XY in data else None + ) + await switch._update_attrs_and_maybe_adapt_lights( + force=True, + context=switch.create_context("accent"), + ) async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: ServiceCall): """Set or unset lights as 'manually controlled'.""" @@ -274,6 +307,7 @@ async def async_setup_entry( sleep_mode_switch = SimpleSwitch("Sleep Mode", False, hass, config_entry) adapt_color_switch = SimpleSwitch("Adapt Color", True, hass, config_entry) adapt_brightness_switch = SimpleSwitch("Adapt Brightness", True, hass, config_entry) + accent_color_switch = SimpleSwitch("Enable Accent Color", False, hass, config_entry) switch = AdaptiveSwitch( hass, config_entry, @@ -281,15 +315,17 @@ async def async_setup_entry( sleep_mode_switch, adapt_color_switch, adapt_brightness_switch, + accent_color_switch, ) data[config_entry.entry_id][SLEEP_MODE_SWITCH] = sleep_mode_switch data[config_entry.entry_id][ADAPT_COLOR_SWITCH] = adapt_color_switch data[config_entry.entry_id][ADAPT_BRIGHTNESS_SWITCH] = adapt_brightness_switch + data[config_entry.entry_id][ENABLE_ACCENT_COLOR_SWITCH] = accent_color_switch data[config_entry.entry_id][SWITCH_DOMAIN] = switch async_add_entities( - [switch, sleep_mode_switch, adapt_color_switch, adapt_brightness_switch], + [switch, sleep_mode_switch, adapt_color_switch, adapt_brightness_switch, accent_color_switch], update_before_add=True, ) @@ -311,6 +347,16 @@ async def async_setup_entry( handle_apply, ) + platform.async_register_entity_service( + SERVICE_SET_ACCENT_COLOR, + { + vol.Optional(CONF_ACCENT_RGB, default=None): vol.Any(None, [int]), + vol.Optional(CONF_ACCENT_HS, default=None): vol.Any(None, [float]), + vol.Optional(CONF_ACCENT_XY, default=None): vol.Any(None, [float]), + }, + handle_set_accent_color, + ) + platform.async_register_entity_service( SERVICE_SET_MANUAL_CONTROL, { @@ -332,8 +378,23 @@ def validate(config_entry: ConfigEntry): value = data.get(key) if value is not None: data[key] = validate_value(value) # Fix the types of the inputs + data[CONF_ACCENT_COLOR] = get_rgb_from_any_form( + config_entry.data[CONF_ACCENT_COLOR_RGB] if CONF_ACCENT_COLOR_RGB in data else None, + config_entry.data[CONF_ACCENT_COLOR_HS] if CONF_ACCENT_COLOR_HS in data else None, + config_entry.data[CONF_ACCENT_COLOR_XY] if CONF_ACCENT_COLOR_XY in data else None + ) return data +def get_rgb_from_any_form( + rgb: Tuple[float, float, float] = None, hs: Tuple[float, float] = None, xy: Tuple[float, float] = None +) -> Union[Tuple[float, float, float], None]: + if rgb is not None: + return rgb + if hs is not None: + return color_hs_to_RGB(hs[0], hs[1]) + if xy is not None: + return color_xy_to_RGB(xy[0], xy[1]) + return None def match_switch_state_event(event: Event, from_or_to_state: List[str]): """Match state event when either 'from_state' or 'to_state' matches.""" @@ -389,6 +450,24 @@ def color_difference_redmean( blue_term = (2 + (255 - r_hat) / 256) * delta_b ** 2 return math.sqrt(red_term + green_term + blue_term) +def interpolate_hue(hue1: float, hue2: float, weight: float) -> float: + """Interpolate between two angles, used for hue values.""" + diff = (hue2 - hue1) % 360 + if diff > 180: + diff = diff - 360 + return ((hue1 + diff * weight) % 360) + +def interpolate_colors( + rgb1: Tuple[float, float, float], rgb2: Tuple[float, float, float], weight: float +) -> Tuple[float, float, float]: + """Interpolate between two RGB colors using the HSV space.""" + hsv1 = color_RGB_to_hsv(*rgb1) + hsv2 = color_RGB_to_hsv(*rgb2) + hue = interpolate_hue(hsv1[0], hsv2[0], weight) + saturation = hsv1[1] + (hsv2[1] - hsv1[1]) * weight + value = hsv1[2] + (hsv2[2] - hsv1[2]) * weight + temp_color = color_hsv_to_RGB(hue, saturation, value) + return temp_color def _attributes_have_changed( light: str, @@ -498,6 +577,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): sleep_mode_switch: SimpleSwitch, adapt_color_switch: SimpleSwitch, adapt_brightness_switch: SimpleSwitch, + accent_color_switch: SimpleSwitch, ): """Initialize the Adaptive Lighting switch.""" self.hass = hass @@ -505,6 +585,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.sleep_mode_switch = sleep_mode_switch self.adapt_color_switch = adapt_color_switch self.adapt_brightness_switch = adapt_brightness_switch + self.accent_color_switch = accent_color_switch data = validate(config_entry) self._name = data[CONF_NAME] @@ -535,6 +616,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): sunset_offset=data[CONF_SUNSET_OFFSET], sunset_time=data[CONF_SUNSET_TIME], time_zone=self.hass.config.time_zone, + accent_color=data[CONF_ACCENT_COLOR], + accent_color_lower_bound=float(data[CONF_ACCENT_COLOR_MIX_MIN])/100.0, + accent_color_upper_bound=float(data[CONF_ACCENT_COLOR_MIX_MAX])/100.0, ) # Set other attributes @@ -590,6 +674,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): EVENT_HOMEASSISTANT_STARTED, self._setup_listeners ) last_state = await self.async_get_last_state() + if (self._sun_light_settings.accent_color is None + and ATTR_SWITCH_ACCENT_COLOR in last_state.attributes + and last_state.attributes[ATTR_SWITCH_ACCENT_COLOR] is not None + and len(last_state.attributes[ATTR_SWITCH_ACCENT_COLOR]) == 3 + ): + # restore previous accent_color as the entry is available + # and the color is not set by the configuration + last_accent_color = last_state.attributes[ATTR_SWITCH_ACCENT_COLOR] + self._sun_light_settings.accent_color = ( + float(last_accent_color[0]), + float(last_accent_color[1]), + float(last_accent_color[2]), + ) is_new_entry = last_state is None # newly added to HA if is_new_entry or last_state.state == STATE_ON: await self.async_turn_on(adapt_lights=not self._only_once) @@ -622,8 +719,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self.sleep_mode_switch.entity_id, self._sleep_mode_switch_state_event, ) + remove_accent_color = async_track_state_change_event( + self.hass, + self.accent_color_switch.entity_id, + self._accent_color_switch_state_event, + ) - self.remove_listeners.extend([remove_interval, remove_sleep]) + self.remove_listeners.extend([remove_interval, remove_sleep, remove_accent_color]) if self._lights: self._expand_light_groups() @@ -706,6 +808,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): transition: Optional[int] = None, adapt_brightness: Optional[bool] = None, adapt_color: Optional[bool] = None, + use_accent_color: Optional[bool] = None, prefer_rgb_color: Optional[bool] = None, force: bool = False, context: Optional[Context] = None, @@ -723,6 +826,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): adapt_brightness = self.adapt_brightness_switch.is_on if adapt_color is None: adapt_color = self.adapt_color_switch.is_on + if use_accent_color is None: + use_accent_color = self.accent_color_switch.is_on if prefer_rgb_color is None: prefer_rgb_color = self._prefer_rgb_color @@ -740,7 +845,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): if ( "color_temp" in features and adapt_color - and not (prefer_rgb_color and "color" in features) + and not ((prefer_rgb_color or use_accent_color) and "color" in features) ): attributes = self.hass.states.get(light).attributes min_mireds, max_mireds = attributes["min_mireds"], attributes["max_mireds"] @@ -799,7 +904,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) assert self.is_on self._settings = self._sun_light_settings.get_settings( - self.sleep_mode_switch.is_on + self.sleep_mode_switch.is_on, self.accent_color_switch.is_on ) self.async_write_ha_state() if lights is None: @@ -859,6 +964,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): context=self.create_context("sleep"), ) + async def _accent_color_switch_state_event(self, event: Event) -> None: + if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): + return + _LOGGER.debug( + "%s: _accent_color_switch_state_event, event: '%s'", self._name, event + ) + # Reset the manually controlled status when the "sleep mode" changes + self.turn_on_off_listener.reset(*self._lights) + await self._update_attrs_and_maybe_adapt_lights( + transition=self._initial_transition, + force=True, + context=self.create_context("accent"), + ) + async def _light_event(self, event: Event) -> None: old_state = event.data.get("old_state") new_state = event.data.get("new_state") @@ -967,7 +1086,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self._state = False -@dataclass(frozen=True) +@dataclass class SunLightSettings: """Track the state of the sun and associated light settings.""" @@ -984,6 +1103,9 @@ class SunLightSettings: sunset_offset: Optional[datetime.timedelta] sunset_time: Optional[datetime.time] time_zone: datetime.tzinfo + accent_color: Optional[Tuple[float, float, float]] + accent_color_lower_bound: Optional[float] + accent_color_upper_bound: Optional[float] def get_sun_events(self, date: datetime.datetime) -> Dict[str, float]: """Get the four sun event's timestamps at 'date'.""" @@ -1079,8 +1201,22 @@ class SunLightSettings: return (delta * percent) + self.min_color_temp return self.min_color_temp + def calc_accented_color( + self, percent: float, rgb_color: Tuple[float, float, float] + ) -> Tuple[float, float, float]: + if self.accent_color is None: + return rgb_color + if percent <= self.accent_color_lower_bound: + return rgb_color + elif percent < self.accent_color_upper_bound: + accent_percent = (percent - float(self.accent_color_lower_bound)) \ + / float(self.accent_color_upper_bound - self.accent_color_lower_bound) + return interpolate_colors(rgb_color, self.accent_color, accent_percent) + else: + return self.accent_color + def get_settings( - self, is_sleep + self, is_sleep, use_accent ) -> Dict[str, Union[float, Tuple[float, float], Tuple[float, float, float]]]: """Get all light settings. @@ -1093,6 +1229,8 @@ class SunLightSettings: rgb_color: Tuple[float, float, float] = color_temperature_to_rgb( color_temp_kelvin ) + if use_accent and self.accent_color is not None and not is_sleep: + rgb_color = self.calc_accented_color(percent, rgb_color) xy_color: Tuple[float, float] = color_RGB_to_xy(*rgb_color) hs_color: Tuple[float, float] = color_xy_to_hs(*xy_color) return { @@ -1103,6 +1241,7 @@ class SunLightSettings: "xy_color": xy_color, "hs_color": hs_color, "sun_position": percent, + ATTR_SWITCH_ACCENT_COLOR: self.accent_color } From 08aa27f0446d226ad784ad125b02f0ecb26e769e Mon Sep 17 00:00:00 2001 From: Michael Kirsch Date: Sun, 15 Nov 2020 12:55:19 +0100 Subject: [PATCH 2/4] add bezier like interpolation --- custom_components/adaptive_lighting/switch.py | 62 +++++++++++++++---- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 32202ab1..f87b45c6 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -248,10 +248,12 @@ async def handle_set_accent_color(switch: AdaptiveSwitch, service_call: ServiceC service_call.data, ) data = service_call.data - switch._sun_light_settings.accent_color = get_rgb_from_any_form( - data[CONF_ACCENT_RGB] if CONF_ACCENT_RGB in data else None, - data[CONF_ACCENT_HS] if CONF_ACCENT_HS in data else None, - data[CONF_ACCENT_XY] if CONF_ACCENT_XY in data else None + switch._sun_light_settings.set_accent_color( + get_rgb_from_any_form( + data[CONF_ACCENT_RGB] if CONF_ACCENT_RGB in data else None, + data[CONF_ACCENT_HS] if CONF_ACCENT_HS in data else None, + data[CONF_ACCENT_XY] if CONF_ACCENT_XY in data else None + ) ) await switch._update_attrs_and_maybe_adapt_lights( force=True, @@ -458,16 +460,13 @@ def interpolate_hue(hue1: float, hue2: float, weight: float) -> float: return ((hue1 + diff * weight) % 360) def interpolate_colors( - rgb1: Tuple[float, float, float], rgb2: Tuple[float, float, float], weight: float + hsv1: Tuple[float, float, float], hsv2: Tuple[float, float, float], weight: float ) -> Tuple[float, float, float]: - """Interpolate between two RGB colors using the HSV space.""" - hsv1 = color_RGB_to_hsv(*rgb1) - hsv2 = color_RGB_to_hsv(*rgb2) + """Interpolate between two HSV colors.""" hue = interpolate_hue(hsv1[0], hsv2[0], weight) saturation = hsv1[1] + (hsv2[1] - hsv1[1]) * weight value = hsv1[2] + (hsv2[2] - hsv1[2]) * weight - temp_color = color_hsv_to_RGB(hue, saturation, value) - return temp_color + return (hue, saturation, value) def _attributes_have_changed( light: str, @@ -620,6 +619,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): accent_color_lower_bound=float(data[CONF_ACCENT_COLOR_MIX_MIN])/100.0, accent_color_upper_bound=float(data[CONF_ACCENT_COLOR_MIX_MAX])/100.0, ) + self._sun_light_settings.set_accent_color(data[CONF_ACCENT_COLOR]) # Set other attributes self._icon = ICON @@ -1107,6 +1107,22 @@ class SunLightSettings: accent_color_lower_bound: Optional[float] accent_color_upper_bound: Optional[float] + _accent_color_hsv: Tuple[float, float, float] = None + _bezier_point_hsv: Tuple[float, float, float] = None + + def set_accent_color(self, accent_color: Tuple[float, float, float]): + self.accent_color = accent_color + if accent_color is not None: + self._accent_color_hsv = color_RGB_to_hsv(*accent_color) + color_upper_temp: float = self.calc_color_temp_kelvin(self.accent_color_upper_bound, False) + color_upper_rgb: Tuple[float, float, float] = color_temperature_to_rgb( + color_upper_temp + ) + self._bezier_point_hsv = color_RGB_to_hsv(*color_upper_rgb) + else: + self._accent_color_hsv = None + self._bezier_point_hsv = None + def get_sun_events(self, date: datetime.datetime) -> Dict[str, float]: """Get the four sun event's timestamps at 'date'.""" @@ -1201,6 +1217,26 @@ class SunLightSettings: return (delta * percent) + self.min_color_temp return self.min_color_temp + def calc_accented_color_interpolation( + self, rgb_color: Tuple[float, float, float], accent_percent: float + ) -> Tuple[float, float, float]: + if self.accent_color is None: + return None + if self._bezier_point_hsv is None or self._accent_color_hsv is None: + self.set_accent_color(self.accent_color) + control_point1 = color_RGB_to_hsv(*rgb_color) + control_point2 = interpolate_colors( + self._bezier_point_hsv, + self._accent_color_hsv, + accent_percent + ) + hsv_color = interpolate_colors( + control_point1, + control_point2, + accent_percent + ) + return color_hsv_to_RGB(*hsv_color) + def calc_accented_color( self, percent: float, rgb_color: Tuple[float, float, float] ) -> Tuple[float, float, float]: @@ -1209,9 +1245,9 @@ class SunLightSettings: if percent <= self.accent_color_lower_bound: return rgb_color elif percent < self.accent_color_upper_bound: - accent_percent = (percent - float(self.accent_color_lower_bound)) \ - / float(self.accent_color_upper_bound - self.accent_color_lower_bound) - return interpolate_colors(rgb_color, self.accent_color, accent_percent) + accent_percent = (percent - self.accent_color_lower_bound) \ + / (self.accent_color_upper_bound - self.accent_color_lower_bound) + return self.calc_accented_color_interpolation(rgb_color, accent_percent) else: return self.accent_color From 94e7e1a6d3682af082d75e0a568f8ea7e9e07490 Mon Sep 17 00:00:00 2001 From: Michael Kirsch Date: Sun, 15 Nov 2020 22:58:45 +0100 Subject: [PATCH 3/4] use xyb interpolation --- custom_components/adaptive_lighting/switch.py | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index f87b45c6..62a8c21f 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -77,11 +77,11 @@ from homeassistant.helpers.sun import get_astral_location from homeassistant.util import slugify from homeassistant.util.color import ( color_hs_to_RGB, - color_hsv_to_RGB, - color_RGB_to_hsv, color_RGB_to_xy, + color_RGB_to_xy_brightness, color_temperature_kelvin_to_mired, color_temperature_to_rgb, + color_xy_brightness_to_RGB, color_xy_to_hs, color_xy_to_RGB, ) @@ -255,10 +255,11 @@ async def handle_set_accent_color(switch: AdaptiveSwitch, service_call: ServiceC data[CONF_ACCENT_XY] if CONF_ACCENT_XY in data else None ) ) - await switch._update_attrs_and_maybe_adapt_lights( - force=True, - context=switch.create_context("accent"), - ) + if switch.is_on: + await switch._update_attrs_and_maybe_adapt_lights( + force=True, + context=switch.create_context("accent"), + ) async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: ServiceCall): """Set or unset lights as 'manually controlled'.""" @@ -452,21 +453,14 @@ def color_difference_redmean( blue_term = (2 + (255 - r_hat) / 256) * delta_b ** 2 return math.sqrt(red_term + green_term + blue_term) -def interpolate_hue(hue1: float, hue2: float, weight: float) -> float: - """Interpolate between two angles, used for hue values.""" - diff = (hue2 - hue1) % 360 - if diff > 180: - diff = diff - 360 - return ((hue1 + diff * weight) % 360) - -def interpolate_colors( - hsv1: Tuple[float, float, float], hsv2: Tuple[float, float, float], weight: float +def interpolate_colors_xyb( + xyb1: Tuple[float, float, float], xyb: Tuple[float, float, float], weight: float ) -> Tuple[float, float, float]: """Interpolate between two HSV colors.""" - hue = interpolate_hue(hsv1[0], hsv2[0], weight) - saturation = hsv1[1] + (hsv2[1] - hsv1[1]) * weight - value = hsv1[2] + (hsv2[2] - hsv1[2]) * weight - return (hue, saturation, value) + x = xyb1[0] + (xyb[0] - xyb1[0]) * weight + y = xyb1[1] + (xyb[1] - xyb1[1]) * weight + b = xyb1[2] + (xyb[2] - xyb1[2]) * weight + return (x, y, b) def _attributes_have_changed( light: str, @@ -674,7 +668,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): EVENT_HOMEASSISTANT_STARTED, self._setup_listeners ) last_state = await self.async_get_last_state() - if (self._sun_light_settings.accent_color is None + is_new_entry = last_state is None # newly added to HA + if (not is_new_entry + and self._sun_light_settings.accent_color is None and ATTR_SWITCH_ACCENT_COLOR in last_state.attributes and last_state.attributes[ATTR_SWITCH_ACCENT_COLOR] is not None and len(last_state.attributes[ATTR_SWITCH_ACCENT_COLOR]) == 3 @@ -687,7 +683,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): float(last_accent_color[1]), float(last_accent_color[2]), ) - is_new_entry = last_state is None # newly added to HA if is_new_entry or last_state.state == STATE_ON: await self.async_turn_on(adapt_lights=not self._only_once) else: @@ -1107,21 +1102,21 @@ class SunLightSettings: accent_color_lower_bound: Optional[float] accent_color_upper_bound: Optional[float] - _accent_color_hsv: Tuple[float, float, float] = None - _bezier_point_hsv: Tuple[float, float, float] = None + _accent_color_xyb: Tuple[float, float, float] = None + _bezier_point_xyb: Tuple[float, float, float] = None def set_accent_color(self, accent_color: Tuple[float, float, float]): self.accent_color = accent_color if accent_color is not None: - self._accent_color_hsv = color_RGB_to_hsv(*accent_color) + self._accent_color_xyb = color_RGB_to_xy_brightness(*accent_color) color_upper_temp: float = self.calc_color_temp_kelvin(self.accent_color_upper_bound, False) color_upper_rgb: Tuple[float, float, float] = color_temperature_to_rgb( color_upper_temp ) - self._bezier_point_hsv = color_RGB_to_hsv(*color_upper_rgb) + self._bezier_point_xyb = color_RGB_to_xy_brightness(*color_upper_rgb) else: - self._accent_color_hsv = None - self._bezier_point_hsv = None + self._accent_color_xyb = None + self._bezier_point_xyb = None def get_sun_events(self, date: datetime.datetime) -> Dict[str, float]: """Get the four sun event's timestamps at 'date'.""" @@ -1222,20 +1217,20 @@ class SunLightSettings: ) -> Tuple[float, float, float]: if self.accent_color is None: return None - if self._bezier_point_hsv is None or self._accent_color_hsv is None: + if self._bezier_point_xyb is None or self._accent_color_xyb is None: self.set_accent_color(self.accent_color) - control_point1 = color_RGB_to_hsv(*rgb_color) - control_point2 = interpolate_colors( - self._bezier_point_hsv, - self._accent_color_hsv, + control_point1 = color_RGB_to_xy_brightness(*rgb_color) + control_point2 = interpolate_colors_xyb( + self._bezier_point_xyb, + self._accent_color_xyb, accent_percent ) - hsv_color = interpolate_colors( + xyb_color = interpolate_colors_xyb( control_point1, control_point2, accent_percent ) - return color_hsv_to_RGB(*hsv_color) + return color_xy_brightness_to_RGB(*xyb_color) def calc_accented_color( self, percent: float, rgb_color: Tuple[float, float, float] From a21b7af89d1e48f4dbd61a8f9316a11e0f871fe7 Mon Sep 17 00:00:00 2001 From: Michael Kirsch Date: Sun, 18 Jul 2021 21:12:34 +0200 Subject: [PATCH 4/4] add xy and hs as alternative color modes --- custom_components/adaptive_lighting/switch.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 7710ed0b..e0470b3c 100755 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -40,6 +40,8 @@ from homeassistant.components.light import ( is_on, COLOR_MODE_RGB, COLOR_MODE_RGBW, + COLOR_MODE_HS, + COLOR_MODE_XY, COLOR_MODE_COLOR_TEMP, COLOR_MODE_BRIGHTNESS, ATTR_SUPPORTED_COLOR_MODES, @@ -459,6 +461,12 @@ def _supported_features(hass: HomeAssistant, light: str): if COLOR_MODE_RGBW in supported_color_modes: supported.add("color") supported.add("brightness") # see above url + if COLOR_MODE_XY in supported_color_modes: + supported.add("color") + supported.add("brightness") # see above url + if COLOR_MODE_HS in supported_color_modes: + supported.add("color") + supported.add("brightness") # see above url if COLOR_MODE_COLOR_TEMP in supported_color_modes: supported.add("color_temp") supported.add("brightness") # see above url