Merge branch 'main' into change_default_config_values

This commit is contained in:
Benjamin Auquite 2023-04-10 13:00:37 -05:00
commit 399dd6a849
4 changed files with 133 additions and 72 deletions

View file

@ -273,6 +273,8 @@ VALIDATION_TUPLES = [
),
]
CONST_COLOR = "color"
def timedelta_as_int(value):
"""Convert a `datetime.timedelta` object to an integer.

View file

@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
"version": "1.10.1"
"version": "1.11.0"
}

View file

@ -23,7 +23,11 @@ from homeassistant.components.light import (
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ATTR_MAX_COLOR_TEMP_KELVIN,
ATTR_MIN_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
ATTR_XY_COLOR,
@ -32,6 +36,7 @@ from homeassistant.components.light import (
COLOR_MODE_HS,
COLOR_MODE_RGB,
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_XY,
)
from homeassistant.components.light import (
@ -129,6 +134,7 @@ from .const import (
CONF_TRANSITION,
CONF_TURN_ON_LIGHTS,
CONF_USE_DEFAULTS,
CONST_COLOR,
DOMAIN,
EXTRA_VALIDATION,
ICON_BRIGHTNESS,
@ -155,6 +161,16 @@ _SUPPORT_OPTS = {
"transition": SUPPORT_TRANSITION,
}
VALID_COLOR_MODES = {
COLOR_MODE_BRIGHTNESS: ATTR_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP_KELVIN,
COLOR_MODE_HS: ATTR_HS_COLOR,
COLOR_MODE_RGB: ATTR_RGB_COLOR,
COLOR_MODE_RGBW: ATTR_RGBW_COLOR,
COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR,
COLOR_MODE_XY: ATTR_XY_COLOR,
}
_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT)
_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))}
@ -424,6 +440,7 @@ def _fire_manual_control_event(
switch.entity_id,
light,
)
switch.turn_on_off_listener.mark_as_manual_control(light)
fire(
f"{DOMAIN}.manual_control",
{ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id},
@ -519,7 +536,6 @@ async def async_setup_entry(
all_lights = _expand_light_groups(switch.hass, lights)
if service_call.data[CONF_MANUAL_CONTROL]:
for light in all_lights:
switch.turn_on_off_listener.mark_as_manual_control(light)
_fire_manual_control_event(switch, light, service_call.context)
else:
switch.turn_on_off_listener.reset(*all_lights)
@ -623,33 +639,51 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
return list(all_lights)
def _supported_to_attributes(supported):
supported_attributes = {}
supports_colors = False
for mode, attr in VALID_COLOR_MODES.items():
if mode not in supported:
continue
supported_attributes[attr] = True
if (
not supports_colors
and mode != COLOR_MODE_BRIGHTNESS
and mode != COLOR_MODE_COLOR_TEMP
):
supports_colors = True
return supported_attributes, supports_colors
def _supported_features(hass: HomeAssistant, light: str):
state = hass.states.get(light)
supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
supported = {
key for key, value in _SUPPORT_OPTS.items() if supported_features & value
legacy_supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
legacy_supported = {
key for key, value in _SUPPORT_OPTS.items() if legacy_supported_features & value
}
supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set())
if COLOR_MODE_RGB in supported_color_modes:
supported.add("color")
supported, supports_colors = _supported_to_attributes(
legacy_supported.union(supported_color_modes)
)
min_kelvin = state.attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN)
max_kelvin = state.attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN)
supported.update(
{
ATTR_MIN_COLOR_TEMP_KELVIN: min_kelvin,
ATTR_MAX_COLOR_TEMP_KELVIN: max_kelvin,
}
)
if supports_colors:
# Adding brightness here, see
# comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011
supported.add("brightness")
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
if COLOR_MODE_BRIGHTNESS in supported_color_modes:
supported.add("brightness")
return supported
supported[ATTR_BRIGHTNESS] = True
if CONST_COLOR not in legacy_supported:
# supports_colors = False
_LOGGER.debug(
"'supported_color_modes' supports color but the legacy 'supported_features'"
" bitfield says we do not. Despite this we'll assume light '%s' supports colors",
)
return supported, supports_colors
def color_difference_redmean(
@ -1014,6 +1048,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if self.turn_on_off_listener.manual_control.get(light)
]
extra_state_attributes.update(self._settings)
timers = self.turn_on_off_listener.auto_reset_manual_control_timers
extra_state_attributes["autoreset_time_remaining"] = {
light: time
for light in self._lights
if (timer := timers.get(light)) and (time := timer.remaining_time()) > 0
}
return extra_state_attributes
def create_context(
@ -1082,9 +1122,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if lock is not None and lock.locked():
_LOGGER.debug("%s: '%s' is locked", self._name, light)
return
service_data = {ATTR_ENTITY_ID: light}
features = _supported_features(self.hass, light)
if transition is None:
transition = self._transition
if adapt_brightness is None:
@ -1094,16 +1131,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if prefer_rgb_color is None:
prefer_rgb_color = self._prefer_rgb_color
# Check transition == 0 to fix #378
if "transition" in features and transition > 0:
service_data[ATTR_TRANSITION] = transition
# The switch might be off and not have _settings set.
self._settings = self._sun_light_settings.get_settings(
self.sleep_mode_switch.is_on, transition
)
if "brightness" in features and adapt_brightness:
# Build service data.
service_data = {ATTR_ENTITY_ID: light}
features, supports_colors = _supported_features(self.hass, light)
# Check transition == 0 to fix #378
if ATTR_TRANSITION in features and transition > 0:
service_data[ATTR_TRANSITION] = transition
if ATTR_BRIGHTNESS in features and adapt_brightness:
brightness = round(255 * self._settings["brightness_pct"] / 100)
service_data[ATTR_BRIGHTNESS] = brightness
@ -1112,36 +1152,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color"
)
if (
"color_temp" in features
ATTR_COLOR_TEMP_KELVIN in features
and adapt_color
and not (prefer_rgb_color and "color" in features)
and not (sleep_rgb and "color" in features)
and not (prefer_rgb_color and supports_colors)
and not (sleep_rgb and supports_colors)
):
_LOGGER.debug("%s: Setting color_temp of light %s", self._name, light)
attributes = self.hass.states.get(light).attributes
min_kelvin = attributes["min_color_temp_kelvin"]
max_kelvin = attributes["max_color_temp_kelvin"]
min_kelvin = features[ATTR_MIN_COLOR_TEMP_KELVIN]
max_kelvin = features[ATTR_MAX_COLOR_TEMP_KELVIN]
color_temp_kelvin = self._settings["color_temp_kelvin"]
color_temp_kelvin = max(min(color_temp_kelvin, max_kelvin), min_kelvin)
service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin
elif "color" in features and adapt_color:
elif supports_colors and adapt_color:
_LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light)
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
context = context or self.create_context("adapt_lights")
if (
self._take_over_control
and self._detect_non_ha_changes
and not force
and await self.turn_on_off_listener.significant_change(
self,
light,
adapt_brightness,
adapt_color,
context,
)
):
return
# See #80. Doesn't check if transitions differ but it does the job.
last_service_data = self.turn_on_off_listener.last_service_data
if not force and last_service_data.get(light) == service_data:
@ -1230,9 +1257,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if not filtered_lights:
return
await self._adapt_lights(filtered_lights, transition, force, context)
await self._update_manual_control_and_maybe_adapt(
filtered_lights, transition, force, context
)
async def _adapt_lights(
async def _update_manual_control_and_maybe_adapt(
self,
lights: list[str],
transition: int | None,
@ -1241,34 +1270,53 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
) -> None:
assert context is not None
_LOGGER.debug(
"%s: '_adapt_lights(%s, %s, force=%s, context.id=%s)' called",
"%s: '_update_manual_control_and_maybe_adapt(%s, %s, force=%s, context.id=%s)' called",
self.name,
lights,
transition,
force,
context.id,
)
adapt_brightness = self.adapt_brightness_switch.is_on
adapt_color = self.adapt_color_switch.is_on
for light in lights:
if not is_on(self.hass, light):
continue
if (
self._take_over_control
and self.turn_on_off_listener.is_manually_controlled(
manually_controlled = self.turn_on_off_listener.is_manually_controlled(
self,
light,
force,
adapt_brightness,
adapt_color,
)
significant_change = (
self._detect_non_ha_changes
and not force
and await self.turn_on_off_listener.significant_change(
self,
light,
force,
self.adapt_brightness_switch.is_on,
self.adapt_color_switch.is_on,
adapt_brightness,
adapt_color,
context,
)
):
_LOGGER.debug(
"%s: '%s' is being manually controlled, stop adapting, context.id=%s.",
self._name,
light,
context.id,
)
continue
await self._adapt_light(light, transition, force=force, context=context)
)
if self._take_over_control and (manually_controlled or significant_change):
if manually_controlled:
_LOGGER.debug(
"%s: '%s' is being manually controlled, stop adapting, context.id=%s.",
self._name,
light,
context.id,
)
else:
_fire_manual_control_event(self, light, context)
else:
await self._adapt_light(light, transition, force=force, context=context)
async def _sleep_mode_switch_state_event(self, event: Event) -> None:
if not match_switch_state_event(event, (STATE_ON, STATE_OFF)):
@ -1894,7 +1942,7 @@ class TurnOnOffListener:
):
# Light was already on and 'light.turn_on' was not called by
# the adaptive_lighting integration.
manual_control = self.mark_as_manual_control(light)
manual_control = True
_fire_manual_control_event(switch, light, turn_on_event.context)
_LOGGER.debug(
"'%s' was already on and 'light.turn_on' was not called by the"
@ -1962,8 +2010,6 @@ class TurnOnOffListener:
light,
context.id,
)
self.mark_as_manual_control(light)
_fire_manual_control_event(switch, light, context, is_async=False)
return True
_LOGGER.debug(
"%s: Light '%s' correctly matches our last adapt's service data, continuing..."
@ -2106,3 +2152,10 @@ class _AsyncSingleShotTimer:
if self.task:
self.task.cancel()
self.callback = None
def remaining_time(self):
"""Return the remaining time before the timer expires."""
if self.start_time is not None:
elapsed_time = (dt_util.utcnow() - self.start_time).total_seconds()
return max(0, self.delay - elapsed_time)
return 0

View file

@ -692,9 +692,15 @@ async def test_auto_reset_manual_control(hass):
await turn_light(True, brightness=1)
await turn_light(True, brightness=10)
assert manual_control[light.entity_id]
assert (
switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0
)
await asyncio.sleep(0.3) # Should be enough time for auto reset
await update()
assert not manual_control[light.entity_id], (light, manual_control)
assert (
light.entity_id not in switch.extra_state_attributes["autoreset_time_remaining"]
)
# Do a couple of quick changes and check that light is not reset
for i in range(3):