Compare commits

...

7 commits

Author SHA1 Message Date
@RubenKelevra
af2790a8db
Merge pull request #244 from Oekn5w/accent_colors
Accent colors by Oekn5w
2022-01-09 00:28:34 +01:00
Michael Kirsch
105a378e0f Merge branch 'master' into accent_colors 2022-01-08 19:35:17 +01:00
Michael Kirsch
a21b7af89d add xy and hs as alternative color modes 2021-07-18 21:12:53 +02:00
Michael Kirsch
cf29d5084f Merge branch 'master' into accent_colors 2021-07-08 20:25:11 +02:00
Michael Kirsch
94e7e1a6d3 use xyb interpolation 2020-11-15 22:58:45 +01:00
Michael Kirsch
08aa27f044 add bezier like interpolation 2020-11-15 12:55:19 +01:00
Michael Kirsch
c552702d36 add accent colors 2020-11-14 16:51:41 +01:00
2 changed files with 195 additions and 6 deletions

View file

@ -37,20 +37,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
@ -71,6 +85,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),

View file

@ -84,10 +84,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_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,
)
import homeassistant.util.dt as dt_util
@ -97,6 +101,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_SLEEP_TRANSITION,
@ -121,8 +136,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,
@ -238,11 +255,30 @@ 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,
context=switch.create_context("service", parent=service_call.context),
)
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.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
)
)
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'."""
@ -304,6 +340,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,
@ -311,15 +348,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,
)
@ -341,6 +380,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,
{
@ -362,8 +411,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."""
@ -442,6 +506,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_colors_xyb(
xyb1: Tuple[float, float, float], xyb: Tuple[float, float, float], weight: float
) -> Tuple[float, float, float]:
"""Interpolate between two HSV colors."""
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,
@ -551,6 +623,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
@ -558,6 +631,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]
@ -594,8 +668,12 @@ 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,
transition=data[CONF_TRANSITION],
)
self._sun_light_settings.set_accent_color(data[CONF_ACCENT_COLOR])
# Set other attributes
self._icon = ICON
@ -651,6 +729,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
last_state = await self.async_get_last_state()
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
):
# 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]),
)
if is_new_entry or last_state.state == STATE_ON:
await self.async_turn_on(adapt_lights=not self._only_once)
else:
@ -682,8 +774,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()
@ -768,6 +865,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,
@ -785,6 +883,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
@ -802,7 +902,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"]
@ -872,7 +972,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
assert self.is_on
self._settings = self._sun_light_settings.get_settings(
self.sleep_mode_switch.is_on, transition
self.sleep_mode_switch.is_on, self.accent_color_switch.is_on, transition
)
self.async_write_ha_state()
if lights is None:
@ -933,6 +1033,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context=self.create_context("sleep", parent=event.context),
)
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")
@ -1041,7 +1155,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
self._state = False
@dataclass(frozen=True)
@dataclass
class SunLightSettings:
"""Track the state of the sun and associated light settings."""
@ -1059,6 +1173,25 @@ class SunLightSettings:
sunset_time: Optional[datetime.time]
time_zone: datetime.tzinfo
transition: int
accent_color: Optional[Tuple[float, float, float]]
accent_color_lower_bound: Optional[float]
accent_color_upper_bound: Optional[float]
_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_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_xyb = color_RGB_to_xy_brightness(*color_upper_rgb)
else:
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'."""
@ -1176,8 +1309,42 @@ 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_xyb is None or self._accent_color_xyb is None:
self.set_accent_color(self.accent_color)
control_point1 = color_RGB_to_xy_brightness(*rgb_color)
control_point2 = interpolate_colors_xyb(
self._bezier_point_xyb,
self._accent_color_xyb,
accent_percent
)
xyb_color = interpolate_colors_xyb(
control_point1,
control_point2,
accent_percent
)
return color_xy_brightness_to_RGB(*xyb_color)
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 - 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
def get_settings(
self, is_sleep, transition
self, is_sleep, use_accent, transition
) -> Dict[str, Union[float, Tuple[float, float], Tuple[float, float, float]]]:
"""Get all light settings.
@ -1190,6 +1357,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 {
@ -1200,6 +1369,7 @@ class SunLightSettings:
"xy_color": xy_color,
"hs_color": hs_color,
"sun_position": percent,
ATTR_SWITCH_ACCENT_COLOR: self.accent_color
}