mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-12 23:04:03 +02:00
Compare commits
25 commits
main
...
add-watche
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea73228ef8 |
||
|
|
f20135b120 |
||
|
|
7069d693b8 |
||
|
|
adde5b5645 | ||
|
|
2c5eaec2d3 | ||
|
|
e773af07ca | ||
|
|
c167f59ccd | ||
|
|
6220df57d1 | ||
|
|
ff292c2c1b | ||
|
|
4fdf42bea1 | ||
|
|
19d4468f63 | ||
|
|
70c90181f0 |
||
|
|
b6a92650a9 | ||
|
|
784eed09b3 | ||
|
|
ae04ab49a8 | ||
|
|
cfbd65f976 |
||
|
|
3c5ea73341 | ||
|
|
2f750694d9 | ||
|
|
429251b45f | ||
|
|
7a9dc8f24a | ||
|
|
37de4244b1 |
||
|
|
d52ea53dcc | ||
|
|
dbc47c8a36 | ||
|
|
2db675a434 | ||
|
|
dced97b5ca |
6 changed files with 297 additions and 125 deletions
64
README.md
64
README.md
|
|
@ -90,37 +90,39 @@ The YAML and frontend configuration methods support all of the options listed be
|
|||
|
||||
<!-- START_OUTPUT -->
|
||||
<!-- THIS CONTENT IS AUTOMATICALLY GENERATED -->
|
||||
| Variable name | Description | Default | Type |
|
||||
|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
|
||||
| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s |
|
||||
| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
|
||||
| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
|
||||
| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
|
||||
| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
|
||||
| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
|
||||
| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
|
||||
| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
|
||||
| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
|
||||
| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
|
||||
| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
|
||||
| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
|
||||
| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
|
||||
| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
|
||||
| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
|
||||
| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
|
||||
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
|
||||
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
|
||||
| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
|
||||
| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
|
||||
| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
|
||||
| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
|
||||
| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
|
||||
| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
|
||||
| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
|
||||
| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
|
||||
| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 |
|
||||
| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` |
|
||||
| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
|
||||
| Variable name | Description | Default | Type |
|
||||
|:-------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
|
||||
| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s |
|
||||
| `watched_lights` | Use this dictionary of lights to check for manually controlled events in addition to the main lights 🌟 Example: {light.watch_light: light.main_light} will fire manually controlled events to light.main_light | `{}` | list of `entity_id`s |
|
||||
| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
|
||||
| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
|
||||
| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
|
||||
| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
|
||||
| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
|
||||
| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
|
||||
| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
|
||||
| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
|
||||
| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
|
||||
| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
|
||||
| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
|
||||
| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
|
||||
| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
|
||||
| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
|
||||
| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
|
||||
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
|
||||
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
|
||||
| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
|
||||
| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
|
||||
| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
|
||||
| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
|
||||
| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
|
||||
| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
|
||||
| `alt_detect_method` | alt_detect_method: When true, will check for any significant changes in the opposite direction of where adaptive-lighting tried to adapt last. This is an alternative to 'detect_non_ha_changes' (default: false) | `False` | `bool` |
|
||||
| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
|
||||
| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
|
||||
| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 |
|
||||
| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` |
|
||||
| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
|
||||
|
||||
<!-- END_OUTPUT -->
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ DOCS[CONF_DETECT_NON_HA_CHANGES] = (
|
|||
"Requires `take_over_control`. 🕵️"
|
||||
)
|
||||
|
||||
CONF_ALT_DETECT_METHOD, DEFAULT_ALT_DETECT_METHOD = "alt_detect_method", False
|
||||
DOCS[CONF_ALT_DETECT_METHOD] = (
|
||||
"alt_detect_method: When true, will check for any significant changes in the opposite direction"
|
||||
" of where adaptive-lighting tried to adapt last."
|
||||
" This is an alternative to 'detect_non_ha_changes' (default: false)"
|
||||
)
|
||||
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = (
|
||||
"include_config_in_attributes",
|
||||
False,
|
||||
|
|
@ -176,6 +182,15 @@ DOCS[CONF_AUTORESET_CONTROL] = (
|
|||
"Set to 0 to disable. ⏲️"
|
||||
)
|
||||
|
||||
CONF_WATCHED_LIGHTS, DEFAULT_WATCHED_LIGHTS = "watched_lights", {}
|
||||
DOCS[CONF_WATCHED_LIGHTS] = (
|
||||
"Use this dictionary of lights to check for manually controlled events"
|
||||
" in addition to the main lights 🌟"
|
||||
" Requires `alt_detect_method` to be True."
|
||||
" Example: `watched_lights: {light.watch_light: light.main_light}` will fire"
|
||||
" manually controlled events to `light.main_light`"
|
||||
)
|
||||
|
||||
SLEEP_MODE_SWITCH = "sleep_mode_switch"
|
||||
ADAPT_COLOR_SWITCH = "adapt_color_switch"
|
||||
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
|
||||
|
|
@ -225,6 +240,7 @@ def int_between(min_int, max_int):
|
|||
|
||||
VALIDATION_TUPLES = [
|
||||
(CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids),
|
||||
(CONF_WATCHED_LIGHTS, DEFAULT_WATCHED_LIGHTS, dict),
|
||||
(CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool),
|
||||
(CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool),
|
||||
(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
|
||||
|
|
@ -262,6 +278,7 @@ VALIDATION_TUPLES = [
|
|||
(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int),
|
||||
(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
|
||||
(CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool),
|
||||
(CONF_ALT_DETECT_METHOD, DEFAULT_ALT_DETECT_METHOD, bool),
|
||||
(CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool),
|
||||
(CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool),
|
||||
(CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.",
|
||||
"data": {
|
||||
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
|
||||
"watched_lights": "watched_lights: Use this dictionary of lights to check for manually controlled events in addition to the main lights 🌟 Example: {light.watch_light: light.main_light} will fire manually controlled events to light.main_light",
|
||||
"prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
|
||||
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
|
||||
"initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
|
||||
|
|
@ -43,6 +44,7 @@
|
|||
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
|
||||
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
|
||||
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
|
||||
"alt_detect_method": "alt_detect_method: alt_detect_method: When true, will check for any significant changes in the opposite direction of where adaptive-lighting tried to adapt last. This is an alternative to 'detect_non_ha_changes' (default: false)",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
|
||||
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
|
||||
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ from .const import (
|
|||
ATTR_TURN_ON_OFF_LISTENER,
|
||||
CONF_ADAPT_DELAY,
|
||||
CONF_ADAPT_UNTIL_SLEEP,
|
||||
CONF_ALT_DETECT_METHOD,
|
||||
CONF_AUTORESET_CONTROL,
|
||||
CONF_DETECT_NON_HA_CHANGES,
|
||||
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES,
|
||||
|
|
@ -134,6 +135,7 @@ from .const import (
|
|||
CONF_TRANSITION,
|
||||
CONF_TURN_ON_LIGHTS,
|
||||
CONF_USE_DEFAULTS,
|
||||
CONF_WATCHED_LIGHTS,
|
||||
CONST_COLOR,
|
||||
DOMAIN,
|
||||
EXTRA_VALIDATION,
|
||||
|
|
@ -708,6 +710,15 @@ def color_difference_redmean(
|
|||
return math.sqrt(red_term + green_term + blue_term)
|
||||
|
||||
|
||||
def check_direction_change(last: int, current: int, last_adapt_value: int) -> bool:
|
||||
_LOGGER.debug("compare direction: current value %s to last value %s", current, last)
|
||||
if last_adapt_value < last: # Value adapting down
|
||||
return current > last or current < last_adapt_value
|
||||
elif last_adapt_value > last: # Value adapting up
|
||||
return current < last or current > last_adapt_value
|
||||
return False
|
||||
|
||||
|
||||
# All comparisons should be done with RGB since
|
||||
# converting anything to color temp is inaccurate.
|
||||
def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]:
|
||||
|
|
@ -750,6 +761,7 @@ def _attributes_have_changed(
|
|||
adapt_brightness: bool,
|
||||
adapt_color: bool,
|
||||
context: Context,
|
||||
last_adapt_attempt=None,
|
||||
) -> bool:
|
||||
if adapt_color:
|
||||
old_attributes, new_attributes = _add_missing_attributes(
|
||||
|
|
@ -764,15 +776,33 @@ def _attributes_have_changed(
|
|||
last_brightness = old_attributes[ATTR_BRIGHTNESS]
|
||||
current_brightness = new_attributes[ATTR_BRIGHTNESS]
|
||||
if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE:
|
||||
_LOGGER.debug(
|
||||
"Brightness of '%s' significantly changed from %s to %s with"
|
||||
" context.id='%s'",
|
||||
light,
|
||||
last_brightness,
|
||||
current_brightness,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
if last_adapt_attempt:
|
||||
changed = check_direction_change(
|
||||
last_brightness,
|
||||
current_brightness,
|
||||
last_adapt_attempt[ATTR_BRIGHTNESS],
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"altdetect: Brightness of '%s' changed from %s to %s intended %s with"
|
||||
" context.id='%s' Significant? %s",
|
||||
light,
|
||||
last_brightness,
|
||||
current_brightness,
|
||||
last_adapt_attempt[ATTR_BRIGHTNESS],
|
||||
context.id,
|
||||
changed,
|
||||
)
|
||||
return changed
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Brightness of '%s' significantly changed from %s to %s with"
|
||||
" context.id='%s'",
|
||||
light,
|
||||
last_brightness,
|
||||
current_brightness,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
|
||||
if (
|
||||
adapt_color
|
||||
|
|
@ -782,15 +812,33 @@ def _attributes_have_changed(
|
|||
last_color_temp = old_attributes[ATTR_COLOR_TEMP_KELVIN]
|
||||
current_color_temp = new_attributes[ATTR_COLOR_TEMP_KELVIN]
|
||||
if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE:
|
||||
_LOGGER.debug(
|
||||
"Color temperature of '%s' significantly changed from %s to %s with"
|
||||
" context.id='%s'",
|
||||
light,
|
||||
last_color_temp,
|
||||
current_color_temp,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
if last_adapt_attempt:
|
||||
changed = check_direction_change(
|
||||
last_color_temp,
|
||||
current_color_temp,
|
||||
last_adapt_attempt[ATTR_COLOR_TEMP_KELVIN],
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"altdetect: Color temperature of '%s' changed from %s to %s intended %s with"
|
||||
" context.id='%s' Significant? %s",
|
||||
light,
|
||||
last_color_temp,
|
||||
current_color_temp,
|
||||
last_adapt_attempt[ATTR_COLOR_TEMP_KELVIN],
|
||||
context.id,
|
||||
changed,
|
||||
)
|
||||
return changed
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Color temperature of '%s' significantly changed from %s to %s with"
|
||||
" context.id='%s'",
|
||||
light,
|
||||
last_color_temp,
|
||||
current_color_temp,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
|
||||
if (
|
||||
adapt_color
|
||||
|
|
@ -910,13 +958,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self._transition = data[CONF_TRANSITION]
|
||||
self._adapt_delay = data[CONF_ADAPT_DELAY]
|
||||
self._send_split_delay = data[CONF_SEND_SPLIT_DELAY]
|
||||
self._watched_lights = data[CONF_WATCHED_LIGHTS]
|
||||
self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
|
||||
self._alt_detect_method = data[CONF_ALT_DETECT_METHOD]
|
||||
self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
|
||||
if not data[CONF_TAKE_OVER_CONTROL] and data[CONF_DETECT_NON_HA_CHANGES]:
|
||||
_LOGGER.warning(
|
||||
"%s: Config mismatch: 'detect_non_ha_changes: true' "
|
||||
"requires 'take_over_control' to be enabled. Adjusting config "
|
||||
"and continuing setup with `take_over_control: true`.",
|
||||
if not data[CONF_TAKE_OVER_CONTROL] and (
|
||||
data[CONF_ALT_DETECT_METHOD] or data[CONF_DETECT_NON_HA_CHANGES]
|
||||
):
|
||||
_LOGGER.warn(
|
||||
"%s: Config mismatch: 'alt_detect_method: true'"
|
||||
" OR 'detect_non_ha_changes: true' are set in config, however required"
|
||||
" variable 'take_over_control' is turned off. Please check your"
|
||||
" configuration to ensure desired functionality. We will now"
|
||||
" enable 'take_over_control' and continue setting up the"
|
||||
" adaptive-lighting integration normally.",
|
||||
self._name,
|
||||
)
|
||||
self._take_over_control = True
|
||||
|
|
@ -1270,6 +1325,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
transition: int | None,
|
||||
force: bool,
|
||||
context: Context | None,
|
||||
adapt_brightness: bool | None = None,
|
||||
adapt_color: bool | None = None,
|
||||
) -> None:
|
||||
assert context is not None
|
||||
_LOGGER.debug(
|
||||
|
|
@ -1284,13 +1341,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
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):
|
||||
all_lights = {k: k for k in lights}
|
||||
all_lights.update(self._watched_lights)
|
||||
|
||||
for wlight, mlight in all_lights.items():
|
||||
if not is_on(self.hass, mlight):
|
||||
continue
|
||||
|
||||
manually_controlled = self.turn_on_off_listener.is_manually_controlled(
|
||||
self,
|
||||
light,
|
||||
wlight,
|
||||
mlight,
|
||||
force,
|
||||
adapt_brightness,
|
||||
adapt_color,
|
||||
|
|
@ -1301,7 +1362,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
and not force
|
||||
and await self.turn_on_off_listener.significant_change(
|
||||
self,
|
||||
light,
|
||||
wlight,
|
||||
mlight,
|
||||
adapt_brightness,
|
||||
adapt_color,
|
||||
context,
|
||||
|
|
@ -1313,13 +1375,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
_LOGGER.debug(
|
||||
"%s: '%s' is being manually controlled, stop adapting, context.id=%s.",
|
||||
self._name,
|
||||
light,
|
||||
mlight,
|
||||
context.id,
|
||||
)
|
||||
else:
|
||||
_fire_manual_control_event(self, light, context)
|
||||
_fire_manual_control_event(self, mlight, context)
|
||||
else:
|
||||
await self._adapt_light(light, transition, force=force, context=context)
|
||||
await self._adapt_light(
|
||||
wlight, 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)):
|
||||
|
|
@ -1922,18 +1986,19 @@ class TurnOnOffListener:
|
|||
def is_manually_controlled(
|
||||
self,
|
||||
switch: AdaptiveSwitch,
|
||||
light: str,
|
||||
wlight: str,
|
||||
mlight: 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)
|
||||
manual_control = self.manual_control.setdefault(wlight, False)
|
||||
if manual_control:
|
||||
# Manually controlled until light is turned on and off
|
||||
return True
|
||||
|
||||
turn_on_event = self.turn_on_event.get(light)
|
||||
turn_on_event = self.turn_on_event.get(wlight)
|
||||
if (
|
||||
turn_on_event is not None
|
||||
and not is_our_context(turn_on_event.context)
|
||||
|
|
@ -1946,13 +2011,13 @@ class TurnOnOffListener:
|
|||
# Light was already on and 'light.turn_on' was not called by
|
||||
# the adaptive_lighting integration.
|
||||
manual_control = True
|
||||
_fire_manual_control_event(switch, light, turn_on_event.context)
|
||||
_fire_manual_control_event(switch, wlight, 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,
|
||||
mlight,
|
||||
turn_on_event.context.id,
|
||||
)
|
||||
return manual_control
|
||||
|
|
@ -1960,7 +2025,8 @@ class TurnOnOffListener:
|
|||
async def significant_change(
|
||||
self,
|
||||
switch: AdaptiveSwitch,
|
||||
light: str,
|
||||
wlight: str,
|
||||
mlight: str,
|
||||
adapt_brightness: bool,
|
||||
adapt_color: bool,
|
||||
context: Context,
|
||||
|
|
@ -1972,35 +2038,77 @@ class TurnOnOffListener:
|
|||
detected, we mark the light as 'manually controlled' until the light
|
||||
or switch is turned 'off' and 'on' again.
|
||||
"""
|
||||
last_service_data = self.last_service_data.get(light)
|
||||
last_service_data = self.last_service_data.get(wlight)
|
||||
if last_service_data is None:
|
||||
return
|
||||
compare_to = functools.partial(
|
||||
_attributes_have_changed,
|
||||
light=light,
|
||||
light=wlight,
|
||||
adapt_brightness=adapt_brightness,
|
||||
adapt_color=adapt_color,
|
||||
context=context,
|
||||
)
|
||||
if switch._alt_detect_method or wlight != mlight:
|
||||
old_states: list[State] = self.last_state_change[wlight]
|
||||
_LOGGER.debug("Total state changes detected: %s", len(old_states))
|
||||
_LOGGER.debug(
|
||||
"%s: 'alt_detect_method: true', check all state changes made to light %s",
|
||||
switch._name,
|
||||
wlight,
|
||||
)
|
||||
for index, old_state in enumerate(old_states):
|
||||
# The first entry of old_states should always be the
|
||||
# same as last_service_data[light], and can be ignored.
|
||||
if index <= 1:
|
||||
continue
|
||||
_LOGGER.debug(
|
||||
"%s: checking for a manual change between index %s and %s...",
|
||||
switch._name,
|
||||
index,
|
||||
index - 1,
|
||||
)
|
||||
prior_state = old_states[index - 1]
|
||||
if compare_to(
|
||||
old_attributes=prior_state.attributes,
|
||||
new_attributes=old_state.attributes,
|
||||
last_adapt_attempt=last_service_data,
|
||||
):
|
||||
_LOGGER.info(
|
||||
"Found unexpected state_change event for %s nr. %s (context.id=%s)"
|
||||
" old_state=%s\nprior_state=%s",
|
||||
wlight,
|
||||
index,
|
||||
context.id,
|
||||
old_state,
|
||||
prior_state,
|
||||
)
|
||||
_LOGGER.info(
|
||||
"We will now set %s as manually controlled. (context.id=%s)",
|
||||
wlight,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
# Update state and check for a manual change not done in HA.
|
||||
# Ensure HASS is correctly updating your light's state with
|
||||
# light.turn_on calls if any problems arise. This
|
||||
# can happen e.g. using zigbee2mqtt with 'report: false' in device settings.
|
||||
if switch._detect_non_ha_changes:
|
||||
if wlight != mlight:
|
||||
return # only supported with alt_detect_method
|
||||
_LOGGER.debug(
|
||||
"%s: 'detect_non_ha_changes: true', calling update_entity(%s)"
|
||||
" and check if it's last adapt succeeded.",
|
||||
switch._name,
|
||||
light,
|
||||
wlight,
|
||||
)
|
||||
# This update_entity probably isn't necessary now that we're checking
|
||||
# if transitions finished from our last adapt.
|
||||
await self.hass.helpers.entity_component.async_update_entity(light)
|
||||
refreshed_state = self.hass.states.get(light)
|
||||
await self.hass.helpers.entity_component.async_update_entity(wlight)
|
||||
refreshed_state = self.hass.states.get(wlight)
|
||||
_LOGGER.debug(
|
||||
"%s: Current state of %s: %s",
|
||||
switch._name,
|
||||
light,
|
||||
wlight,
|
||||
refreshed_state,
|
||||
)
|
||||
changed = compare_to(
|
||||
|
|
@ -2010,7 +2118,7 @@ class TurnOnOffListener:
|
|||
if changed:
|
||||
_LOGGER.debug(
|
||||
"State of '%s' didn't change wrt 'last_service_data' (context.id=%s)",
|
||||
light,
|
||||
wlight,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
|
|
@ -2018,7 +2126,7 @@ class TurnOnOffListener:
|
|||
"%s: Light '%s' correctly matches our last adapt's service data, continuing..."
|
||||
" context.id=%s.",
|
||||
switch._name,
|
||||
light,
|
||||
wlight,
|
||||
context.id,
|
||||
)
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.",
|
||||
"data": {
|
||||
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
|
||||
"watched_lights": "watched_lights: Use this dictionary of lights to check for manually controlled events in addition to the main lights 🌟 Example: {light.watch_light: light.main_light} will fire manually controlled events to light.main_light",
|
||||
"prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
|
||||
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
|
||||
"initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
|
||||
|
|
@ -44,6 +45,7 @@
|
|||
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
|
||||
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
|
||||
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
|
||||
"alt_detect_method": "alt_detect_method: alt_detect_method: When true, will check for any significant changes in the opposite direction of where adaptive-lighting tried to adapt last. This is an alternative to 'detect_non_ha_changes' (default: false)",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
|
||||
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
|
||||
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from homeassistant.components.adaptive_lighting.const import (
|
|||
ADAPT_BRIGHTNESS_SWITCH,
|
||||
ADAPT_COLOR_SWITCH,
|
||||
ATTR_TURN_ON_OFF_LISTENER,
|
||||
CONF_ALT_DETECT_METHOD,
|
||||
CONF_AUTORESET_CONTROL,
|
||||
CONF_DETECT_NON_HA_CHANGES,
|
||||
CONF_INITIAL_TRANSITION,
|
||||
|
|
@ -23,6 +24,7 @@ from homeassistant.components.adaptive_lighting.const import (
|
|||
CONF_SUNRISE_OFFSET,
|
||||
CONF_SUNRISE_TIME,
|
||||
CONF_SUNSET_TIME,
|
||||
CONF_TAKE_OVER_CONTROL,
|
||||
CONF_TRANSITION,
|
||||
CONF_TURN_ON_LIGHTS,
|
||||
CONF_USE_DEFAULTS,
|
||||
|
|
@ -221,7 +223,9 @@ async def setup_lights_and_switch(hass, extra_conf=None):
|
|||
CONF_SUNSET_TIME: datetime.time(SUNSET.hour),
|
||||
CONF_INITIAL_TRANSITION: 0,
|
||||
CONF_TRANSITION: 0,
|
||||
CONF_DETECT_NON_HA_CHANGES: True,
|
||||
CONF_ALT_DETECT_METHOD: False,
|
||||
CONF_DETECT_NON_HA_CHANGES: False,
|
||||
CONF_TAKE_OVER_CONTROL: True,
|
||||
CONF_PREFER_RGB_COLOR: False,
|
||||
CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp
|
||||
**(extra_conf or {}),
|
||||
|
|
@ -748,7 +752,7 @@ async def test_manual_control(hass):
|
|||
@pytest.mark.dependency(depends=[*GLOBAL_TEST_DEPENDENCIES, "test_manual_control"])
|
||||
async def test_auto_reset_manual_control(hass):
|
||||
switch, (light, *_) = await setup_lights_and_switch(
|
||||
hass, {CONF_AUTORESET_CONTROL: 0.1}
|
||||
hass, {CONF_AUTORESET_CONTROL: 0.2}
|
||||
)
|
||||
context = switch.create_context("test") # needs to be passed to update method
|
||||
manual_control = switch.turn_on_off_listener.manual_control
|
||||
|
|
@ -993,7 +997,7 @@ async def test_state_change_handlers(hass):
|
|||
|
||||
# [Config options]:
|
||||
transition_used = 2
|
||||
total_events = 5
|
||||
total_events = 6
|
||||
|
||||
async def set_brightness(val: int):
|
||||
# 'Unsafe' set but we know what we're doing.
|
||||
|
|
@ -1114,58 +1118,95 @@ async def test_state_change_handlers(hass):
|
|||
assert listener.transition_timers.get(ENTITY_LIGHT)
|
||||
|
||||
# 5. Execute some checks during a transition
|
||||
_LOGGER.debug("Test detect_non_ha_changes:")
|
||||
switch._take_over_control = True
|
||||
assert switch._take_over_control
|
||||
switch._detect_non_ha_changes = True
|
||||
assert switch._detect_non_ha_changes
|
||||
await asyncio.sleep(transition_used / 3)
|
||||
# Ensure the timer still exists
|
||||
timer = listener.transition_timers.get(ENTITY_LIGHT)
|
||||
assert timer and timer.is_running()
|
||||
last_service_data = deepcopy(current_service_data)
|
||||
await update()
|
||||
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
await update()
|
||||
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
timer = listener.transition_timers.get(ENTITY_LIGHT)
|
||||
assert timer and timer.is_running()
|
||||
# Ensure the light did not adapt during the transition.
|
||||
assert last_service_data == current_service_data
|
||||
|
||||
for i in range(2):
|
||||
if i == 0:
|
||||
_LOGGER.debug("Test detect_non_ha_changes before a transition:")
|
||||
switch._take_over_control = True
|
||||
assert switch._take_over_control
|
||||
switch._detect_non_ha_changes = True
|
||||
assert switch._detect_non_ha_changes
|
||||
switch._alt_detect_method = False
|
||||
assert not switch._alt_detect_method
|
||||
elif i == 1:
|
||||
_LOGGER.debug("Test alt_detect_method before a transition:")
|
||||
switch._take_over_control = True
|
||||
assert switch._take_over_control
|
||||
switch._detect_non_ha_changes = False
|
||||
assert not switch._detect_non_ha_changes
|
||||
switch._alt_detect_method = True
|
||||
assert switch._alt_detect_method
|
||||
await asyncio.sleep(transition_used / 3)
|
||||
# Ensure the timer still exists
|
||||
timer = listener.transition_timers.get(ENTITY_LIGHT)
|
||||
assert timer and timer.is_running()
|
||||
last_service_data = deepcopy(current_service_data)
|
||||
await update()
|
||||
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
await update()
|
||||
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
timer = listener.transition_timers.get(ENTITY_LIGHT)
|
||||
assert timer and timer.is_running()
|
||||
# Ensure the light did not adapt during the transition.
|
||||
assert last_service_data == current_service_data
|
||||
|
||||
# 6. Assert everything after the transition finishes.
|
||||
await asyncio.sleep(transition_used)
|
||||
assert listener.last_state_change.get(ENTITY_LIGHT)
|
||||
assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events
|
||||
# Timer should be done and reset now.
|
||||
# This is the assert that I can't fix.
|
||||
timer = listener.transition_timers.get(ENTITY_LIGHT)
|
||||
assert not timer or not timer.is_running()
|
||||
|
||||
# build last service data
|
||||
await update(force=False)
|
||||
for i in range(2):
|
||||
if i == 0:
|
||||
_LOGGER.debug("Test detect_non_ha_changes after a transition:")
|
||||
switch._take_over_control = True
|
||||
assert switch._take_over_control
|
||||
switch._detect_non_ha_changes = True
|
||||
assert switch._detect_non_ha_changes
|
||||
switch._alt_detect_method = False
|
||||
assert not switch._alt_detect_method
|
||||
elif i == 1:
|
||||
_LOGGER.debug("Test alt_detect_method after a transition:")
|
||||
switch._take_over_control = True
|
||||
assert switch._take_over_control
|
||||
switch._detect_non_ha_changes = False
|
||||
assert not switch._detect_non_ha_changes
|
||||
switch._alt_detect_method = True
|
||||
assert switch._alt_detect_method
|
||||
|
||||
# force=True should not reset manual control.
|
||||
await turn_light(True, brightness=40)
|
||||
await turn_light(True, brightness=20)
|
||||
await update(force=False)
|
||||
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
await update(force=True)
|
||||
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
assert listener.last_state_change.get(ENTITY_LIGHT)
|
||||
if i == 1:
|
||||
total_events = 2
|
||||
assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events
|
||||
# Timer should be done and reset now.
|
||||
timer = listener.transition_timers.get(ENTITY_LIGHT)
|
||||
assert not timer or not timer.is_running()
|
||||
|
||||
# turn light off then on should reset manual control.
|
||||
await turn_light(False)
|
||||
await turn_light(True)
|
||||
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
# build last service data
|
||||
await update(force=False)
|
||||
|
||||
await turn_light(True, brightness=50)
|
||||
_LOGGER.debug("Test: Brightness set to %s", 50)
|
||||
# force=True should not reset manual control.
|
||||
await turn_light(True, brightness=40)
|
||||
await turn_light(True, brightness=20)
|
||||
await update(force=False)
|
||||
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
await update(force=True)
|
||||
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
|
||||
# On next update ENTITY_LIGHT should be marked as manually controlled
|
||||
await update(force=False)
|
||||
assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
|
||||
assert switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) is not None
|
||||
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
# turn light off then on should reset manual control.
|
||||
await turn_light(False)
|
||||
await turn_light(True)
|
||||
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
|
||||
await turn_light(True, brightness=50)
|
||||
_LOGGER.debug("Test: Brightness set to %s", 50)
|
||||
|
||||
# On next update ENTITY_LIGHT should be marked as manually controlled
|
||||
await update(force=False)
|
||||
assert (
|
||||
switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
|
||||
)
|
||||
assert (
|
||||
switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) is not None
|
||||
)
|
||||
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
|
||||
|
||||
@pytest.mark.dependency(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue