define a adapt_brightness and adapt_color switch and watch for color or brightness related data in light.turn_on

This commit is contained in:
Bas Nijholt 2020-10-21 23:05:12 +02:00
commit 8b6527fd14
2 changed files with 138 additions and 73 deletions

View file

@ -12,9 +12,6 @@ SUN_EVENT_MIDNIGHT = "solar_midnight"
CONF_NAME, DEFAULT_NAME = "name", "default"
CONF_LIGHTS, DEFAULT_LIGHTS = "lights", []
CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS = "adapt_brightness", True
CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP = "adapt_color_temp", True
CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR = "adapt_rgb_color", True
CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = (
"detect_non_ha_changes",
False,
@ -37,9 +34,13 @@ CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True
CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45
SLEEP_MODE_SWITCH = "sleep_mode_switch"
ADAPT_COLOR_SWITCH = "adapt_color_switch"
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_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"
SERVICE_SET_MANUAL_CONTROL = "set_manual_control"
CONF_MANUAL_CONTROL = "manual_control"
@ -56,9 +57,6 @@ def int_between(min_int, max_int):
VALIDATION_TUPLES = [
(CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids),
(CONF_ADAPT_BRIGHTNESS, DEFAULT_ADAPT_BRIGHTNESS, bool),
(CONF_ADAPT_COLOR_TEMP, DEFAULT_ADAPT_COLOR_TEMP, bool),
(CONF_ADAPT_RGB_COLOR, DEFAULT_ADAPT_RGB_COLOR, bool),
(CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool),
(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
(CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION),

View file

@ -19,9 +19,17 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_BRIGHTNESS_STEP,
ATTR_BRIGHTNESS_STEP_PCT,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_KELVIN,
ATTR_RGB_COLOR,
ATTR_TRANSITION,
ATTR_WHITE_VALUE,
ATTR_XY_COLOR,
DOMAIN as LIGHT_DOMAIN,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
@ -64,6 +72,7 @@ from homeassistant.helpers.event import (
)
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_RGB_to_xy,
color_temperature_kelvin_to_mired,
@ -73,10 +82,11 @@ from homeassistant.util.color import (
import homeassistant.util.dt as dt_util
from .const import (
ADAPT_BRIGHTNESS_SWITCH,
ADAPT_COLOR_SWITCH,
ATTR_ADAPT_BRIGHTNESS,
ATTR_ADAPT_COLOR,
ATTR_TURN_ON_OFF_LISTENER,
CONF_ADAPT_BRIGHTNESS,
CONF_ADAPT_COLOR_TEMP,
CONF_ADAPT_RGB_COLOR,
CONF_DETECT_NON_HA_CHANGES,
CONF_INITIAL_TRANSITION,
CONF_INTERVAL,
@ -129,6 +139,23 @@ BRIGHTNESS_CHANGE = 25 # ≈10% of total range
COLOR_TEMP_CHANGE = 20 # ≈5% of total range
RGB_REDMEAN_CHANGE = 80 # ≈10% of total range
COLOR_ATTRS = { # Should ATTR_PROFILE be in here?
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_KELVIN,
ATTR_RGB_COLOR,
ATTR_WHITE_VALUE, # Should this be here?
ATTR_XY_COLOR,
}
BRIGHTNESS_ATTRS = {
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_BRIGHTNESS_STEP,
ATTR_BRIGHTNESS_STEP_PCT,
}
# Keep a short domain version for the context instances (which can only be 36 chars)
_DOMAIN_SHORT = "adapt_lgt"
@ -165,9 +192,8 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall):
await switch._adapt_light( # pylint: disable=protected-access
light,
data[CONF_TRANSITION],
data[CONF_ADAPT_BRIGHTNESS],
data[CONF_ADAPT_COLOR_TEMP],
data[CONF_ADAPT_RGB_COLOR],
data[ATTR_ADAPT_BRIGHTNESS],
data[ATTR_ADAPT_COLOR],
force=True,
)
@ -214,13 +240,27 @@ async def async_setup_entry(
data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass)
turn_on_off_listener = data[ATTR_TURN_ON_OFF_LISTENER]
sleep_mode_switch = AdaptiveSleepModeSwitch(hass, config_entry)
switch = AdaptiveSwitch(hass, config_entry, turn_on_off_listener, sleep_mode_switch)
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)
switch = AdaptiveSwitch(
hass,
config_entry,
turn_on_off_listener,
sleep_mode_switch,
adapt_color_switch,
adapt_brightness_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][SWITCH_DOMAIN] = switch
async_add_entities([switch, sleep_mode_switch], update_before_add=True)
async_add_entities(
[switch, sleep_mode_switch, adapt_color_switch, adapt_brightness_switch],
update_before_add=True,
)
# Register `apply` service
platform = entity_platform.current_platform.get()
@ -232,9 +272,8 @@ async def async_setup_entry(
CONF_TRANSITION,
default=switch._initial_transition, # pylint: disable=protected-access
): VALID_TRANSITION,
vol.Optional(CONF_ADAPT_BRIGHTNESS, default=True): cv.boolean,
vol.Optional(CONF_ADAPT_COLOR_TEMP, default=True): cv.boolean,
vol.Optional(CONF_ADAPT_RGB_COLOR, default=True): cv.boolean,
vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean,
vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean,
vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean,
},
handle_apply,
@ -264,7 +303,7 @@ def validate(config_entry: ConfigEntry):
return data
def match_state_event(event: Event, from_or_to_state: List[str]):
def match_switch_state_event(event: Event, from_or_to_state: List[str]):
"""Match state event when either 'from_state' or 'to_state' matches."""
old_state = event.data.get("old_state")
from_state_match = old_state is not None and old_state.state in from_or_to_state
@ -324,8 +363,7 @@ def _attributes_have_changed(
old_attributes: Dict[str, Any],
new_attributes: Dict[str, Any],
adapt_brightness: bool,
adapt_color_temp: bool,
adapt_rgb_color: bool,
adapt_color: bool,
context: Context,
) -> bool:
if (
@ -347,7 +385,7 @@ def _attributes_have_changed(
return True
if (
adapt_color_temp
adapt_color
and ATTR_COLOR_TEMP in old_attributes
and ATTR_COLOR_TEMP in new_attributes
):
@ -365,7 +403,7 @@ def _attributes_have_changed(
return True
if (
adapt_rgb_color
adapt_color
and ATTR_RGB_COLOR in old_attributes
and ATTR_RGB_COLOR in new_attributes
):
@ -407,20 +445,21 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
hass,
config_entry: ConfigEntry,
turn_on_off_listener: TurnOnOffListener,
sleep_mode_switch: AdaptiveSleepModeSwitch,
sleep_mode_switch: SimpleSwitch,
adapt_color_switch: SimpleSwitch,
adapt_brightness_switch: SimpleSwitch,
):
"""Initialize the Adaptive Lighting switch."""
self.hass = hass
self.turn_on_off_listener = turn_on_off_listener
self.sleep_mode_switch = sleep_mode_switch
self.adapt_color_switch = adapt_color_switch
self.adapt_brightness_switch = adapt_brightness_switch
data = validate(config_entry)
self._name = data[CONF_NAME]
self._lights = data[CONF_LIGHTS]
self._adapt_brightness = data[CONF_ADAPT_BRIGHTNESS]
self._adapt_color_temp = data[CONF_ADAPT_COLOR_TEMP]
self._adapt_rgb_color = data[CONF_ADAPT_RGB_COLOR]
self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
self._initial_transition = data[CONF_INITIAL_TRANSITION]
self._interval = data[CONF_INTERVAL]
@ -527,12 +566,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
remove_interval = async_track_time_interval(
self.hass, self._async_update_at_interval, self._interval
)
remove_sleep = async_track_state_change_event(
self.hass,
self.sleep_mode_switch.entity_id,
self._sleep_state_event,
)
self.remove_listeners.extend([remove_interval, remove_sleep])
remove_simple_switches = [
async_track_state_change_event(
self.hass,
switch.entity_id,
self._simple_switch_state_event,
)
for switch in (
self.sleep_mode_switch,
self.adapt_brightness_switch,
self.adapt_color_switch,
)
]
self.remove_listeners.extend([remove_interval, *remove_simple_switches])
if self._lights:
self._expand_light_groups()
@ -614,8 +661,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
light: str,
transition: Optional[int] = None,
adapt_brightness: Optional[bool] = None,
adapt_color_temp: Optional[bool] = None,
adapt_rgb_color: Optional[bool] = None,
adapt_color: Optional[bool] = None,
force: bool = False,
context: Optional[Context] = None,
) -> None:
@ -629,11 +675,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if transition is None:
transition = self._transition
if adapt_brightness is None:
adapt_brightness = self._adapt_brightness
if adapt_color_temp is None:
adapt_color_temp = self._adapt_color_temp
if adapt_rgb_color is None:
adapt_rgb_color = self._adapt_rgb_color
adapt_brightness = self.adapt_brightness_switch.is_on
if adapt_color is None:
adapt_color = self.adapt_color_switch.is_on
if "transition" in features:
service_data[ATTR_TRANSITION] = transition
@ -644,7 +688,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if (
"color_temp" in features
and adapt_color_temp
and adapt_color
and not (self._prefer_rgb_color and "color" in features)
):
attributes = self.hass.states.get(light).attributes
@ -652,7 +696,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
color_temp_mired = self._settings["color_temp_mired"]
color_temp_mired = max(min(color_temp_mired, max_mireds), min_mireds)
service_data[ATTR_COLOR_TEMP] = color_temp_mired
elif "color" in features and adapt_rgb_color:
elif "color" in features and adapt_color:
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
context = context or self.create_context("adapt_lights")
@ -662,9 +706,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and not force
and await self.turn_on_off_listener.significant_change(
light,
self._adapt_brightness,
self._adapt_color_temp,
self._adapt_rgb_color,
adapt_brightness,
adapt_color,
context,
)
):
@ -732,6 +775,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and self.turn_on_off_listener.is_manually_controlled(
light,
force,
self.adapt_brightness_switch.is_on,
self.adapt_color_switch.is_on,
)
):
_LOGGER.debug(
@ -743,15 +788,22 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
continue
await self._adapt_light(light, transition, force=force, context=context)
async def _sleep_state_event(self, event: Event) -> None:
if not match_state_event(event, (STATE_ON, STATE_OFF)):
async def _simple_switch_state_event(self, event: Event) -> None:
if not match_switch_state_event(event, (STATE_ON, STATE_OFF)):
return
_LOGGER.debug("%s: _sleep_state_event, event: '%s'", self._name, event)
self.turn_on_off_listener.reset(*self._lights)
which = {
self.sleep_mode_switch.entity_id: "sleep_sw",
self.adapt_color_switch.entity_id: "color_sw",
self.adapt_brightness_switch.entity_id: "brigt_sw",
}[event.data[ATTR_ENTITY_ID]]
_LOGGER.debug("%s: _simple_switch_state_event, event: '%s'", self._name, event)
if which == "sleep_sw":
# 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("sleep"),
context=self.create_context(which),
)
async def _light_event(self, event: Event) -> None:
@ -805,26 +857,32 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self.turn_on_off_listener.reset(entity_id)
class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity):
class SimpleSwitch(SwitchEntity, RestoreEntity):
"""Representation of a Adaptive Lighting switch."""
def __init__(self, hass: HomeAssistant, config_entry):
def __init__(
self, which: str, initial_state: bool, hass: HomeAssistant, config_entry
):
"""Initialize the Adaptive Lighting switch."""
self.hass = hass
data = validate(config_entry)
self._name = data[CONF_NAME]
self._icon = ICON
self._state = None
self._which = which
self._unique_id = f"{self._name}_{slugify(self._which)}"
self._name = f"Adaptive Lighting {which}: {self._name}"
self._initial_state = initial_state
@property
def name(self):
"""Return the name of the device if any."""
return f"Adaptive Lighting Sleep Mode: {self._name}"
return self._name
@property
def unique_id(self):
"""Return the unique ID of entity."""
return f"{self._name}_sleep_mode"
return self._unique_id
@property
def icon(self) -> str:
@ -839,10 +897,15 @@ class AdaptiveSleepModeSwitch(SwitchEntity, RestoreEntity):
async def async_added_to_hass(self) -> None:
"""Call when entity about to be added to hass."""
last_state = await self.async_get_last_state()
if last_state is None or STATE_OFF: # newly added to HA
await self.async_turn_off()
else:
if last_state is None: # newly added to HA
if self._initial_state:
await self.async_turn_on()
else:
await self.async_turn_off()
elif STATE_ON:
await self.async_turn_on()
elif STATE_OFF:
await self.async_turn_off()
async def async_turn_on(self, **kwargs) -> None:
"""Turn on adaptive lighting sleep mode."""
@ -1124,6 +1187,8 @@ class TurnOnOffListener:
self,
light: str,
force: bool,
adapt_brightness: bool,
adapt_color: bool,
) -> bool:
"""Check if the light has been 'on' and is now manually controlled."""
manual_control = self.manual_control.setdefault(light, False)
@ -1137,26 +1202,29 @@ class TurnOnOffListener:
and not is_our_context(turn_on_event.context)
and not force
):
# Light was already on and 'light.turn_on' was not called by
# the adaptive_lighting integration.
manual_control = self.manual_control[light] = True
_fire_manual_control_event(self.hass, light, turn_on_event.context)
_LOGGER.debug(
"'%s' was already on and 'light.turn_on' was not called by the"
" adaptive_lighting integration (context.id='%s'), the Adaptive"
" Lighting will stop adapting the light until the switch or the"
" light turns off and then on again.",
light,
turn_on_event.context.id,
)
keys = turn_on_event.data[ATTR_SERVICE_DATA].keys()
if (adapt_color and COLOR_ATTRS.intersection(keys)) or (
adapt_brightness and BRIGHTNESS_ATTRS.intersection(keys)
):
# Light was already on and 'light.turn_on' was not called by
# the adaptive_lighting integration.
manual_control = self.manual_control[light] = True
_fire_manual_control_event(self.hass, light, turn_on_event.context)
_LOGGER.debug(
"'%s' was already on and 'light.turn_on' was not called by the"
" adaptive_lighting integration (context.id='%s'), the Adaptive"
" Lighting will stop adapting the light until the switch or the"
" light turns off and then on again.",
light,
turn_on_event.context.id,
)
return manual_control
async def significant_change(
self,
light: str,
adapt_brightness: bool,
adapt_color_temp: bool,
adapt_rgb_color: bool,
adapt_color: bool,
context: Context,
) -> bool:
"""Has the light made a significant change since last update.
@ -1176,8 +1244,7 @@ class TurnOnOffListener:
light=light,
new_attributes=new_state.attributes,
adapt_brightness=adapt_brightness,
adapt_color_temp=adapt_color_temp,
adapt_rgb_color=adapt_rgb_color,
adapt_color=adapt_color,
context=context,
)
for index, old_state in enumerate(old_states):