add accent colors

This commit is contained in:
Michael Kirsch 2020-11-14 16:51:41 +01:00
commit c552702d36
2 changed files with 164 additions and 6 deletions

View file

@ -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),

View file

@ -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
}