mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 22:34:04 +02:00
Individual manual control of brightness and color (#1356)
* refactor: introduce light control parameter enum * refactor: replace manual control flag with parameter enum * test: update deprecated color temp attribute * build: set execution bits on task scripts * feat: individual manual control of brightness and color * test: add tests for individual manual control evaluation * fix: sequential manual changes not always detected If multiple attributes of a light were changed within an interval, only the last change was detected because the check in the interval only used the latest event. For example, if there was a brightness change and a following color change, only the color attribute was detected as manually controlled. To fix this, the manual control attribute flags are now set directly from the event handler so that all events are processed. * fix: invalid service description * docs: fix missing space in config description * refactor: pluralize multivalued bitmask enum name
This commit is contained in:
parent
2f599d6e1b
commit
f84ee445b7
12 changed files with 846 additions and 293 deletions
91
README.md
91
README.md
|
|
@ -105,46 +105,47 @@ The YAML and frontend configuration methods support all of the options listed be
|
|||
|
||||
<!-- OUTPUT:START -->
|
||||
<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->
|
||||
| Variable name | Description | Default | Type |
|
||||
|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------|
|
||||
| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s |
|
||||
| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
|
||||
| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
|
||||
| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
|
||||
| `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 |
|
||||
| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
|
||||
| `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 |
|
||||
| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `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` |
|
||||
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
|
||||
| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` |
|
||||
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier 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 sunsets. 🌇 | `None` | `str` |
|
||||
| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` |
|
||||
| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
|
||||
| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` |
|
||||
| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` |
|
||||
| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` |
|
||||
| `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` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` |
|
||||
| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
|
||||
| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
|
||||
| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `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` |
|
||||
| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
|
||||
| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` |
|
||||
| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` |
|
||||
| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
|
||||
| Variable name | Description | Default | Type |
|
||||
|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------|
|
||||
| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s |
|
||||
| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
|
||||
| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
|
||||
| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
|
||||
| `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 |
|
||||
| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
|
||||
| `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 |
|
||||
| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `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` |
|
||||
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
|
||||
| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` |
|
||||
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier 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 sunsets. 🌇 | `None` | `str` |
|
||||
| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` |
|
||||
| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
|
||||
| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` |
|
||||
| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` |
|
||||
| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` |
|
||||
| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒 | `True` | `bool` |
|
||||
| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` |
|
||||
| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` |
|
||||
| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
|
||||
| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
|
||||
| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `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` |
|
||||
| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
|
||||
| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` |
|
||||
| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` |
|
||||
| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
|
||||
|
||||
<!-- OUTPUT:END -->
|
||||
|
||||
|
|
@ -210,11 +211,11 @@ adaptive_lighting:
|
|||
|
||||
<!-- OUTPUT:START -->
|
||||
<!-- ⚠️ This content is auto-generated by `markdown-code-runner`. -->
|
||||
| Service data attribute | Description | Required | Type |
|
||||
|:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------|
|
||||
| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s |
|
||||
| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s |
|
||||
| `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool |
|
||||
| Service data attribute | Description | Required | Type |
|
||||
|:-------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------|:-----------------------------------------|
|
||||
| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s |
|
||||
| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s |
|
||||
| `manual_control` | Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒 | ❌ | bool or one of `['brightness', 'color']` |
|
||||
|
||||
<!-- OUTPUT:END -->
|
||||
#### `adaptive_lighting.change_switch_settings`
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ def _type_to_str(type_: Any) -> str: # noqa: PLR0911
|
|||
return "bool"
|
||||
if isinstance(type_, vol.All):
|
||||
return _format_voluptuous_instance(type_)
|
||||
if isinstance(type_, vol.Any):
|
||||
return " or ".join(_type_to_str(t) for t in type_.validators)
|
||||
if isinstance(type_, vol.In):
|
||||
return f"one of `{type_.container}`"
|
||||
if isinstance(type_, selector.SelectSelector):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
import logging
|
||||
from collections.abc import AsyncGenerator
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Literal
|
||||
from enum import IntFlag, auto
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
|
|
@ -12,6 +13,8 @@ from homeassistant.components.light import (
|
|||
ATTR_BRIGHTNESS_STEP_PCT,
|
||||
ATTR_COLOR_NAME,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_EFFECT,
|
||||
ATTR_FLASH,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_RGBW_COLOR,
|
||||
|
|
@ -45,6 +48,41 @@ BRIGHTNESS_ATTRS = {
|
|||
ServiceData = dict[str, Any]
|
||||
|
||||
|
||||
class LightControlAttributes(IntFlag):
|
||||
"""Attributes of lights that the adaptation engine can control."""
|
||||
|
||||
NONE = 0
|
||||
BRIGHTNESS = auto()
|
||||
COLOR = auto()
|
||||
|
||||
ALL = BRIGHTNESS | COLOR
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the attributes."""
|
||||
if self == LightControlAttributes.NONE:
|
||||
return "NONE"
|
||||
|
||||
return "|".join(
|
||||
member.name
|
||||
for member in type(self)
|
||||
if member is not LightControlAttributes.NONE
|
||||
and member in self
|
||||
and member.name is not None
|
||||
)
|
||||
|
||||
def has_any(self) -> bool:
|
||||
"""Determine whether any attribute is selected."""
|
||||
return self != LightControlAttributes.NONE
|
||||
|
||||
def has_none(self) -> bool:
|
||||
"""Determine whether no attribute is selected."""
|
||||
return self == LightControlAttributes.NONE
|
||||
|
||||
def has_all(self) -> bool:
|
||||
"""Determine whether all attributes are selected."""
|
||||
return (self & LightControlAttributes.ALL) == LightControlAttributes.ALL
|
||||
|
||||
|
||||
def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]:
|
||||
"""Splits the service data by the adapted attributes.
|
||||
|
||||
|
|
@ -145,7 +183,7 @@ class AdaptationData:
|
|||
service_call_datas: AsyncGenerator[ServiceData]
|
||||
force: bool
|
||||
max_length: int
|
||||
which: Literal["brightness", "color", "both"]
|
||||
attributes: LightControlAttributes
|
||||
initial_sleep: bool = False
|
||||
|
||||
async def next_service_call_data(self) -> ServiceData | None:
|
||||
|
|
@ -161,7 +199,7 @@ class AdaptationData:
|
|||
f"sleep_time={self.sleep_time}, "
|
||||
f"force={self.force}, "
|
||||
f"max_length={self.max_length}, "
|
||||
f"which={self.which}, "
|
||||
f"attributes={self.attributes}, "
|
||||
f"initial_sleep={self.initial_sleep}"
|
||||
")"
|
||||
)
|
||||
|
|
@ -171,20 +209,25 @@ class NoColorOrBrightnessInServiceDataError(Exception):
|
|||
"""Exception raised when no color or brightness attributes are found in service data."""
|
||||
|
||||
|
||||
def _identify_lighting_type(
|
||||
def _identify_light_control_attributes(
|
||||
service_data: ServiceData,
|
||||
) -> Literal["brightness", "color", "both"]:
|
||||
) -> LightControlAttributes:
|
||||
"""Extract the 'which' attribute from the service data."""
|
||||
has_brightness = ATTR_BRIGHTNESS in service_data
|
||||
has_color = any(attr in service_data for attr in COLOR_ATTRS)
|
||||
if has_brightness and has_color:
|
||||
return "both"
|
||||
|
||||
parameters = LightControlAttributes.NONE
|
||||
|
||||
if has_brightness:
|
||||
return "brightness"
|
||||
parameters |= LightControlAttributes.BRIGHTNESS
|
||||
if has_color:
|
||||
return "color"
|
||||
msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}"
|
||||
raise NoColorOrBrightnessInServiceDataError(msg)
|
||||
parameters |= LightControlAttributes.COLOR
|
||||
|
||||
if parameters == LightControlAttributes.NONE:
|
||||
msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}"
|
||||
raise NoColorOrBrightnessInServiceDataError(msg)
|
||||
|
||||
return parameters
|
||||
|
||||
|
||||
def prepare_adaptation_data(
|
||||
|
|
@ -220,7 +263,7 @@ def prepare_adaptation_data(
|
|||
filter_by_state,
|
||||
)
|
||||
|
||||
lighting_type = _identify_lighting_type(service_data)
|
||||
attributes = _identify_light_control_attributes(service_data)
|
||||
|
||||
return AdaptationData(
|
||||
entity_id=entity_id,
|
||||
|
|
@ -229,5 +272,58 @@ def prepare_adaptation_data(
|
|||
service_call_datas=service_data_iterator,
|
||||
force=force,
|
||||
max_length=service_datas_length,
|
||||
which=lighting_type,
|
||||
attributes=attributes,
|
||||
)
|
||||
|
||||
|
||||
def manual_control_event_attribute_to_flags(
|
||||
manual_control_attribute: bool | str,
|
||||
) -> LightControlAttributes:
|
||||
"""Convert manual control event data to light control attributes."""
|
||||
if isinstance(manual_control_attribute, bool) and manual_control_attribute:
|
||||
return LightControlAttributes.ALL
|
||||
if manual_control_attribute == "brightness":
|
||||
return LightControlAttributes.BRIGHTNESS
|
||||
if manual_control_attribute == "color":
|
||||
return LightControlAttributes.COLOR
|
||||
return LightControlAttributes.NONE
|
||||
|
||||
|
||||
def has_brightness_attribute(
|
||||
service_data: ServiceData,
|
||||
) -> bool:
|
||||
"""Determine whether the service data contains brightness attributes."""
|
||||
return any(attr in BRIGHTNESS_ATTRS for attr in service_data)
|
||||
|
||||
|
||||
def has_color_attribute(
|
||||
service_data: ServiceData,
|
||||
) -> bool:
|
||||
"""Determine whether the service data contains color attributes."""
|
||||
return any(attr in COLOR_ATTRS for attr in service_data)
|
||||
|
||||
|
||||
def has_effect_attribute(
|
||||
service_data: ServiceData,
|
||||
) -> bool:
|
||||
"""Determine whether the service data contains effect attributes."""
|
||||
return ATTR_FLASH in service_data or ATTR_EFFECT in service_data
|
||||
|
||||
|
||||
def get_light_control_attributes(
|
||||
service_data: ServiceData,
|
||||
) -> LightControlAttributes:
|
||||
"""Get the light control attributes affected by the service call data."""
|
||||
parameters = LightControlAttributes.NONE
|
||||
|
||||
if has_brightness_attribute(service_data):
|
||||
parameters |= LightControlAttributes.BRIGHTNESS
|
||||
|
||||
if has_color_attribute(service_data):
|
||||
parameters |= LightControlAttributes.COLOR
|
||||
|
||||
if has_effect_attribute(service_data):
|
||||
parameters |= LightControlAttributes.BRIGHTNESS
|
||||
parameters |= LightControlAttributes.COLOR
|
||||
|
||||
return parameters
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Constants for the Adaptive Lighting integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
|
@ -16,6 +17,14 @@ ICON_SLEEP = "mdi:sleep"
|
|||
|
||||
DOMAIN = "adaptive_lighting"
|
||||
|
||||
|
||||
class TakeOverControlMode(Enum):
|
||||
"""Modes for pausing adaptation when control of a light is taken over externally."""
|
||||
|
||||
PAUSE_ALL = "pause_all"
|
||||
PAUSE_CHANGED = "pause_changed"
|
||||
|
||||
|
||||
DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"}
|
||||
|
||||
|
||||
|
|
@ -34,6 +43,7 @@ DOCS[CONF_DETECT_NON_HA_CHANGES] = (
|
|||
"Needs `take_over_control` enabled. 🕵️ "
|
||||
"Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result "
|
||||
"in lights turning on unexpectedly. "
|
||||
"Note that this calls `homeassistant.update_entity` every `interval`! "
|
||||
"Disable this feature if you encounter such issues."
|
||||
)
|
||||
|
||||
|
|
@ -188,9 +198,19 @@ DOCS[CONF_BRIGHTNESS_MODE_TIME_LIGHT] = (
|
|||
|
||||
CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True
|
||||
DOCS[CONF_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`! 🔒"
|
||||
"Pause adaptation of individual lights and hand over (manual) control to other sources that "
|
||||
"issue `light.turn_on` calls for lights that are on. 🔒"
|
||||
)
|
||||
|
||||
CONF_TAKE_OVER_CONTROL_MODE, DEFAULT_TAKE_OVER_CONTROL_MODE = (
|
||||
"take_over_control_mode",
|
||||
TakeOverControlMode.PAUSE_ALL.value,
|
||||
)
|
||||
DOCS[CONF_TAKE_OVER_CONTROL_MODE] = (
|
||||
"The adaptation pausing mode when other sources change brightness and/or color of lights. "
|
||||
"`pause_all` always pauses both brightness and color adaptation. "
|
||||
"`pause_changed` pauses the adaptation of only the changed attributes and continues adapting "
|
||||
"unchanged attributes, e.g., continues color adaptation when only brightness was changed."
|
||||
)
|
||||
|
||||
CONF_TRANSITION, DEFAULT_TRANSITION = "transition", 45
|
||||
|
|
@ -284,8 +304,9 @@ DOCS_MANUAL_CONTROL = {
|
|||
"light as being `manually controlled`. 📝",
|
||||
CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the "
|
||||
"switch are selected. 💡",
|
||||
CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") the '
|
||||
'light from the "manual_control" list. 🔒',
|
||||
CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") all '
|
||||
'adapted attributes of the light from the "manual_control" list, or the '
|
||||
"name of an attribute for selective addition. 🔒",
|
||||
}
|
||||
|
||||
DOCS_APPLY = {
|
||||
|
|
@ -351,6 +372,20 @@ VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [
|
|||
(CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK, int),
|
||||
(CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT, int),
|
||||
(CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool),
|
||||
(
|
||||
CONF_TAKE_OVER_CONTROL_MODE,
|
||||
DEFAULT_TAKE_OVER_CONTROL_MODE,
|
||||
selector.SelectSelector( # type: ignore[arg-type]
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
TakeOverControlMode.PAUSE_ALL.value,
|
||||
TakeOverControlMode.PAUSE_CHANGED.value,
|
||||
],
|
||||
multiple=False,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
),
|
||||
(CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool),
|
||||
(
|
||||
CONF_AUTORESET_CONTROL,
|
||||
|
|
@ -446,6 +481,9 @@ SET_MANUAL_CONTROL_SCHEMA = vol.Schema(
|
|||
{
|
||||
vol.Optional(CONF_ENTITY_ID): cv.entity_ids, # type: ignore[arg-type]
|
||||
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, # type: ignore[arg-type]
|
||||
vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean,
|
||||
vol.Optional(CONF_MANUAL_CONTROL, default=True): vol.Any(
|
||||
cv.boolean,
|
||||
vol.In(["brightness", "color"]),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ set_manual_control:
|
|||
domain: light
|
||||
multiple: true
|
||||
manual_control:
|
||||
description: Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒
|
||||
description: Whether to add ("true") or remove ("false") all adapted attributes of the light from the "manual_control" list, or the name of an attribute for selective addition. 🔒
|
||||
example: true
|
||||
default: true
|
||||
selector:
|
||||
|
|
@ -220,13 +220,22 @@ change_switch_settings:
|
|||
selector:
|
||||
time: null
|
||||
take_over_control:
|
||||
description: 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`! 🔒
|
||||
description: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒
|
||||
required: false
|
||||
example: true
|
||||
selector:
|
||||
boolean: null
|
||||
take_over_control_mode:
|
||||
description: The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.
|
||||
required: false
|
||||
example: pause_changed
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- pause_all
|
||||
- pause_changed
|
||||
detect_non_ha_changes:
|
||||
description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.'
|
||||
description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.'
|
||||
required: false
|
||||
example: false
|
||||
selector:
|
||||
|
|
|
|||
|
|
@ -52,8 +52,9 @@
|
|||
"brightness_mode": "brightness_mode",
|
||||
"brightness_mode_time_dark": "brightness_mode_time_dark",
|
||||
"brightness_mode_time_light": "brightness_mode_time_light",
|
||||
"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`! 🔒",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
|
||||
"take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒",
|
||||
"take_over_control_mode": "take_over_control_mode",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.",
|
||||
"autoreset_control_seconds": "autoreset_control_seconds",
|
||||
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
|
||||
"adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️",
|
||||
|
|
@ -85,6 +86,7 @@
|
|||
"brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
|
||||
"brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
|
||||
"brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
|
||||
"take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.",
|
||||
"autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
|
||||
"send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
|
||||
"adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️"
|
||||
|
|
@ -144,7 +146,7 @@
|
|||
"name": "lights"
|
||||
},
|
||||
"manual_control": {
|
||||
"description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒",
|
||||
"description": "Whether to add (\"true\") or remove (\"false\") all adapted attributes of the light from the \"manual_control\" list, or the name of an attribute for selective addition. 🔒",
|
||||
"name": "manual_control"
|
||||
}
|
||||
}
|
||||
|
|
@ -250,11 +252,15 @@
|
|||
"name": "min_sunset_time"
|
||||
},
|
||||
"take_over_control": {
|
||||
"description": "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`! 🔒",
|
||||
"description": "Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒",
|
||||
"name": "take_over_control"
|
||||
},
|
||||
"take_over_control_mode": {
|
||||
"description": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.",
|
||||
"name": "take_over_control_mode"
|
||||
},
|
||||
"detect_non_ha_changes": {
|
||||
"description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
|
||||
"description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.",
|
||||
"name": "detect_non_ha_changes"
|
||||
},
|
||||
"transition": {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import logging
|
|||
import zoneinfo
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
|
@ -17,8 +17,6 @@ import voluptuous as vol
|
|||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_EFFECT,
|
||||
ATTR_FLASH,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
ATTR_TRANSITION,
|
||||
|
|
@ -76,10 +74,12 @@ from homeassistant.util.color import (
|
|||
)
|
||||
|
||||
from .adaptation_utils import (
|
||||
BRIGHTNESS_ATTRS,
|
||||
COLOR_ATTRS,
|
||||
AdaptationData,
|
||||
LightControlAttributes,
|
||||
ServiceData,
|
||||
get_light_control_attributes,
|
||||
has_effect_attribute,
|
||||
manual_control_event_attribute_to_flags,
|
||||
prepare_adaptation_data,
|
||||
)
|
||||
from .color_and_brightness import SunLightSettings
|
||||
|
|
@ -127,6 +127,7 @@ from .const import (
|
|||
CONF_SUNSET_OFFSET,
|
||||
CONF_SUNSET_TIME,
|
||||
CONF_TAKE_OVER_CONTROL,
|
||||
CONF_TAKE_OVER_CONTROL_MODE,
|
||||
CONF_TRANSITION,
|
||||
CONF_TURN_ON_LIGHTS,
|
||||
CONF_USE_DEFAULTS,
|
||||
|
|
@ -143,6 +144,7 @@ from .const import (
|
|||
SLEEP_MODE_SWITCH,
|
||||
TURNING_OFF_DELAY,
|
||||
VALIDATION_TUPLES,
|
||||
TakeOverControlMode,
|
||||
apply_service_schema,
|
||||
replace_none_str,
|
||||
)
|
||||
|
|
@ -359,27 +361,6 @@ async def handle_change_switch_settings(
|
|||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _fire_manual_control_event(
|
||||
switch: AdaptiveSwitch,
|
||||
light: str,
|
||||
context: Context,
|
||||
) -> None:
|
||||
"""Fire an event that 'light' is marked as manual_control."""
|
||||
hass = switch.hass
|
||||
_LOGGER.debug(
|
||||
"'adaptive_lighting.manual_control' event fired for %s for light %s",
|
||||
switch.entity_id,
|
||||
light,
|
||||
)
|
||||
switch.manager.mark_as_manual_control(light)
|
||||
hass.bus.async_fire(
|
||||
f"{DOMAIN}.manual_control",
|
||||
{ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id},
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry( # noqa: PLR0915
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
|
|
@ -497,9 +478,21 @@ async def async_setup_entry( # noqa: PLR0915
|
|||
all_lights = switch.lights
|
||||
else:
|
||||
all_lights = _expand_light_groups(hass, lights)
|
||||
if service_call.data[CONF_MANUAL_CONTROL]:
|
||||
|
||||
manual_attributes = manual_control_event_attribute_to_flags(
|
||||
service_call.data[CONF_MANUAL_CONTROL],
|
||||
)
|
||||
|
||||
if manual_attributes:
|
||||
for light in all_lights:
|
||||
_fire_manual_control_event(switch, light, service_call.context)
|
||||
switch.manager.set_manual_control_attributes(
|
||||
light,
|
||||
manual_attributes,
|
||||
)
|
||||
switch.fire_manual_control_event(
|
||||
light,
|
||||
service_call.context,
|
||||
)
|
||||
else:
|
||||
switch.manager.reset(*all_lights)
|
||||
if switch.is_on:
|
||||
|
|
@ -753,36 +746,32 @@ def _attributes_have_changed(
|
|||
light: str,
|
||||
old_attributes: dict[str, Any],
|
||||
new_attributes: dict[str, Any],
|
||||
adapt_brightness: bool,
|
||||
adapt_color: bool,
|
||||
context: Context,
|
||||
) -> bool:
|
||||
) -> LightControlAttributes:
|
||||
# 2023-11-19: HA core no longer removes light domain attributes when off
|
||||
# so we must protect for `None` here
|
||||
# see https://github.com/home-assistant/core/pull/101946
|
||||
|
||||
changed_attributes = LightControlAttributes.NONE
|
||||
|
||||
# Check for color mode changes BEFORE attribute conversion
|
||||
# This detects external changes like Hue scenes switching from color_temp to RGB
|
||||
# See: https://github.com/basnijholt/adaptive-lighting/issues/1275
|
||||
if adapt_color and _has_color_mode_changed(
|
||||
if _has_color_mode_changed(
|
||||
light,
|
||||
old_attributes,
|
||||
new_attributes,
|
||||
context,
|
||||
):
|
||||
return True
|
||||
changed_attributes |= LightControlAttributes.COLOR
|
||||
|
||||
if adapt_color:
|
||||
if LightControlAttributes.COLOR not in changed_attributes:
|
||||
old_attributes, new_attributes = _add_missing_attributes(
|
||||
old_attributes,
|
||||
new_attributes,
|
||||
)
|
||||
|
||||
if (
|
||||
adapt_brightness
|
||||
and old_attributes.get(ATTR_BRIGHTNESS)
|
||||
and new_attributes.get(ATTR_BRIGHTNESS)
|
||||
):
|
||||
if old_attributes.get(ATTR_BRIGHTNESS) and new_attributes.get(ATTR_BRIGHTNESS):
|
||||
last_brightness = old_attributes[ATTR_BRIGHTNESS]
|
||||
current_brightness = new_attributes[ATTR_BRIGHTNESS]
|
||||
if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE:
|
||||
|
|
@ -794,10 +783,10 @@ def _attributes_have_changed(
|
|||
current_brightness,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
changed_attributes |= LightControlAttributes.BRIGHTNESS
|
||||
|
||||
if (
|
||||
adapt_color
|
||||
LightControlAttributes.COLOR not in changed_attributes
|
||||
and old_attributes.get(ATTR_COLOR_TEMP_KELVIN)
|
||||
and new_attributes.get(ATTR_COLOR_TEMP_KELVIN)
|
||||
):
|
||||
|
|
@ -812,10 +801,10 @@ def _attributes_have_changed(
|
|||
current_color_temp,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
changed_attributes |= LightControlAttributes.COLOR
|
||||
|
||||
if (
|
||||
adapt_color
|
||||
LightControlAttributes.COLOR not in changed_attributes
|
||||
and old_attributes.get(ATTR_RGB_COLOR)
|
||||
and new_attributes.get(ATTR_RGB_COLOR)
|
||||
):
|
||||
|
|
@ -831,9 +820,9 @@ def _attributes_have_changed(
|
|||
current_rgb_color,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
changed_attributes |= LightControlAttributes.COLOR
|
||||
|
||||
return False
|
||||
return changed_attributes
|
||||
|
||||
|
||||
class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
||||
|
|
@ -937,6 +926,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self._name,
|
||||
)
|
||||
self._take_over_control = True
|
||||
self._take_over_control_mode = TakeOverControlMode(
|
||||
data[CONF_TAKE_OVER_CONTROL_MODE],
|
||||
)
|
||||
self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
|
||||
self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON]
|
||||
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
|
||||
|
|
@ -1203,12 +1195,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
context: Context | None = None,
|
||||
) -> AdaptationData | None:
|
||||
"""Prepare `AdaptationData` for adapting a light."""
|
||||
adaptation_attributes = self.manager.get_adaption_control_attributes(
|
||||
self,
|
||||
light,
|
||||
)
|
||||
|
||||
if transition is None:
|
||||
transition = self._transition
|
||||
if adapt_brightness is None:
|
||||
adapt_brightness = self.adapt_brightness_switch.is_on
|
||||
adapt_brightness = (
|
||||
LightControlAttributes.BRIGHTNESS in adaptation_attributes
|
||||
)
|
||||
if adapt_color is None:
|
||||
adapt_color = self.adapt_color_switch.is_on
|
||||
adapt_color = LightControlAttributes.COLOR in adaptation_attributes
|
||||
if prefer_rgb_color is None:
|
||||
prefer_rgb_color = self._prefer_rgb_color
|
||||
|
||||
|
|
@ -1375,7 +1374,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
to cancel an ongoing adaptation when a light is turned off.
|
||||
"""
|
||||
# Prevent overlap of multiple adaptation sequences
|
||||
self.manager.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which)
|
||||
self.manager.cancel_ongoing_adaptation_calls(data.entity_id)
|
||||
_LOGGER.debug(
|
||||
"%s: execute_cancellable_adaptation_calls with data: %s",
|
||||
self._name,
|
||||
|
|
@ -1384,9 +1383,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
# Execute adaptation calls within a task
|
||||
try:
|
||||
task = asyncio.ensure_future(self._execute_adaptation_calls(data))
|
||||
if data.which in ("both", "brightness"):
|
||||
if LightControlAttributes.BRIGHTNESS in data.attributes:
|
||||
self.manager.adaptation_tasks_brightness[data.entity_id] = task
|
||||
if data.which in ("both", "color"):
|
||||
if LightControlAttributes.COLOR in data.attributes:
|
||||
self.manager.adaptation_tasks_color[data.entity_id] = task
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
|
|
@ -1397,7 +1396,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
data,
|
||||
)
|
||||
|
||||
async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912
|
||||
async def _update_attrs_and_maybe_adapt_lights(
|
||||
self,
|
||||
*,
|
||||
context: Context,
|
||||
|
|
@ -1468,49 +1467,26 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
if not filtered_lights:
|
||||
return
|
||||
|
||||
adapt_brightness = self.adapt_brightness_switch.is_on
|
||||
adapt_color = self.adapt_color_switch.is_on
|
||||
assert isinstance(adapt_brightness, bool)
|
||||
assert isinstance(adapt_color, bool)
|
||||
tasks: list[asyncio.Task[None]] = []
|
||||
for light in filtered_lights:
|
||||
manually_controlled = (
|
||||
self._take_over_control
|
||||
and self.manager.is_manually_controlled(
|
||||
self,
|
||||
light,
|
||||
force,
|
||||
adapt_brightness,
|
||||
adapt_color,
|
||||
)
|
||||
await self.manager.update_manually_controlled_from_untracked_change(
|
||||
self,
|
||||
light,
|
||||
force,
|
||||
context,
|
||||
)
|
||||
if manually_controlled:
|
||||
|
||||
# Performance optimization: Skip adaptation task if all attributes are
|
||||
# manually controlled and the task wouldn't actually do anything.
|
||||
if self.manager.get_adaption_control_attributes(self, light).has_none():
|
||||
_LOGGER.debug(
|
||||
"%s: '%s' is being manually controlled, stop adapting, context.id=%s.",
|
||||
"%s: '%s' is being manually controlled, skip adaptation, context.id=%s.",
|
||||
self._name,
|
||||
light,
|
||||
context.id,
|
||||
)
|
||||
continue
|
||||
|
||||
significant_change = (
|
||||
self._take_over_control
|
||||
and self._detect_non_ha_changes
|
||||
and not force
|
||||
# Note: This call updates the state of the light
|
||||
# so it might suddenly be off.
|
||||
and await self.manager.significant_change(
|
||||
self,
|
||||
light,
|
||||
adapt_brightness,
|
||||
adapt_color,
|
||||
context,
|
||||
)
|
||||
)
|
||||
if significant_change:
|
||||
_fire_manual_control_event(self, light, context)
|
||||
continue
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:"
|
||||
" '%s' with transition %s and context.id=%s",
|
||||
|
|
@ -1553,7 +1529,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
entity_id,
|
||||
event.context.id,
|
||||
)
|
||||
self.manager.mark_as_manual_control(entity_id)
|
||||
self.manager.set_manual_control_attributes(entity_id)
|
||||
return
|
||||
|
||||
if (
|
||||
|
|
@ -1607,6 +1583,28 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
force=True,
|
||||
)
|
||||
|
||||
def fire_manual_control_event(
|
||||
self,
|
||||
light: str,
|
||||
context: Context,
|
||||
) -> None:
|
||||
"""Fire an event that 'light' is marked as manual_control."""
|
||||
_LOGGER.debug(
|
||||
"'adaptive_lighting.manual_control' event fired for %s for light %s",
|
||||
self.entity_id,
|
||||
light,
|
||||
)
|
||||
manual_attributes = self.manager.get_manual_control_attributes(light)
|
||||
self.hass.bus.async_fire(
|
||||
f"{DOMAIN}.manual_control",
|
||||
{
|
||||
ATTR_ENTITY_ID: light,
|
||||
SWITCH_DOMAIN: self.entity_id,
|
||||
CONF_MANUAL_CONTROL: manual_attributes,
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
class SimpleSwitch(SwitchEntity, RestoreEntity):
|
||||
"""Representation of a Adaptive Lighting switch."""
|
||||
|
|
@ -1711,7 +1709,7 @@ class AdaptiveLightingManager:
|
|||
# Locks that prevent light adjusting when waiting for a light to 'turn_off'
|
||||
self.turn_off_locks: dict[str, asyncio.Lock] = {}
|
||||
# Tracks which lights are manually controlled
|
||||
self.manual_control: dict[str, bool] = {}
|
||||
self.manual_control: dict[str, LightControlAttributes] = {}
|
||||
# Track 'state_changed' events of self.lights resulting from this integration
|
||||
self.our_last_state_on_change: dict[str, list[State]] = {}
|
||||
# Track last 'service_data' to 'light.turn_on' resulting from this integration
|
||||
|
|
@ -1954,10 +1952,7 @@ class AdaptiveLightingManager:
|
|||
# were skipped by us
|
||||
return
|
||||
|
||||
if (
|
||||
ATTR_EFFECT in service_data[CONF_PARAMS]
|
||||
or ATTR_FLASH in service_data[CONF_PARAMS]
|
||||
):
|
||||
if has_effect_attribute(service_data[CONF_PARAMS]):
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
|
|
@ -2204,10 +2199,26 @@ class AdaptiveLightingManager:
|
|||
)
|
||||
self.auto_reset_manual_control_times[light] = time
|
||||
|
||||
def mark_as_manual_control(self, light: str) -> None:
|
||||
"""Mark a light as manually controlled."""
|
||||
_LOGGER.debug("Marking '%s' as manually controlled.", light)
|
||||
self.manual_control[light] = True
|
||||
def get_manual_control_attributes(
|
||||
self,
|
||||
light: str,
|
||||
) -> LightControlAttributes:
|
||||
"""Get the attributes for a light that are manually controlled."""
|
||||
return self.manual_control.get(light, LightControlAttributes.NONE)
|
||||
|
||||
def set_manual_control_attributes(
|
||||
self,
|
||||
light: str,
|
||||
attributes: LightControlAttributes = LightControlAttributes.ALL,
|
||||
) -> None:
|
||||
"""Mark attributes of a light as manually controlled."""
|
||||
_LOGGER.debug(
|
||||
"Light %s: Setting manual control attributes to %s (from %s).",
|
||||
light,
|
||||
attributes,
|
||||
self.manual_control[light],
|
||||
)
|
||||
self.manual_control[light] = attributes
|
||||
delay = self.auto_reset_manual_control_times.get(light)
|
||||
|
||||
async def reset() -> None:
|
||||
|
|
@ -2228,35 +2239,87 @@ class AdaptiveLightingManager:
|
|||
transition=switch.initial_transition,
|
||||
force=True,
|
||||
)
|
||||
assert not self.manual_control[light]
|
||||
assert self.manual_control[light] == LightControlAttributes.NONE
|
||||
|
||||
self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset)
|
||||
|
||||
def add_manual_control_attributes(
|
||||
self,
|
||||
light: str,
|
||||
attributes: LightControlAttributes,
|
||||
) -> None:
|
||||
"""Add attributes to the manual control status of a light."""
|
||||
current = self.get_manual_control_attributes(light)
|
||||
_LOGGER.debug(
|
||||
"Light %s: Adding manual control attributes %s (current: %s).",
|
||||
light,
|
||||
attributes,
|
||||
current,
|
||||
)
|
||||
new = current | attributes
|
||||
self.set_manual_control_attributes(light, new)
|
||||
|
||||
def get_adaption_control_attributes(
|
||||
self,
|
||||
switch: AdaptiveSwitch,
|
||||
light: str,
|
||||
) -> LightControlAttributes:
|
||||
"""Get the attributes that should be adapted for a light.
|
||||
|
||||
Determines the attributes that should actually be adapted from the attributes
|
||||
marked as manually controlled, the state of adaptation switches, and the adaptation
|
||||
configuration.
|
||||
|
||||
Example 1: When no attributes are marked as manually controlled and all adaptation
|
||||
switches are on, all attributes are returned.
|
||||
|
||||
Example 2: When no attributes are marked as manually controlled and the brightness
|
||||
adaptation switch is off, only the color attribute is returned.
|
||||
|
||||
Example 3: When only brightness is marked as manually controlled, but the configuration
|
||||
specifies to pause all adaptations on manual change, no attributes are returned so that
|
||||
color is also not adapted.
|
||||
"""
|
||||
denied_adaptation_attributes = self.get_manual_control_attributes(light)
|
||||
|
||||
if (
|
||||
denied_adaptation_attributes.has_any()
|
||||
and switch._take_over_control_mode == TakeOverControlMode.PAUSE_ALL
|
||||
):
|
||||
# Extend to pausing all only if there is at least one manually controlled attribute
|
||||
denied_adaptation_attributes = LightControlAttributes.ALL
|
||||
|
||||
enabled_adaptation_attributes = (
|
||||
LightControlAttributes.BRIGHTNESS
|
||||
if switch.adapt_brightness_switch.is_on
|
||||
else LightControlAttributes.NONE
|
||||
) | (
|
||||
LightControlAttributes.COLOR
|
||||
if switch.adapt_color_switch.is_on
|
||||
else LightControlAttributes.NONE
|
||||
)
|
||||
|
||||
return (
|
||||
LightControlAttributes.ALL
|
||||
& ~denied_adaptation_attributes
|
||||
& enabled_adaptation_attributes
|
||||
)
|
||||
|
||||
def cancel_ongoing_adaptation_calls(
|
||||
self,
|
||||
light_id: str,
|
||||
which: Literal["color", "brightness", "both"] = "both",
|
||||
) -> None:
|
||||
"""Cancel ongoing adaptation service calls for a specific light entity."""
|
||||
brightness_task = self.adaptation_tasks_brightness.get(light_id)
|
||||
color_task = self.adaptation_tasks_color.get(light_id)
|
||||
if (
|
||||
which in ("both", "brightness")
|
||||
and brightness_task is not None
|
||||
and not brightness_task.done()
|
||||
):
|
||||
if brightness_task is not None and not brightness_task.done():
|
||||
_LOGGER.debug(
|
||||
"Cancelled ongoing brightness adaptation calls (%s) for '%s'",
|
||||
brightness_task,
|
||||
light_id,
|
||||
)
|
||||
brightness_task.cancel()
|
||||
if (
|
||||
which in ("both", "color")
|
||||
and color_task is not None
|
||||
and color_task is not brightness_task
|
||||
and not color_task.done()
|
||||
):
|
||||
if color_task is not None and not color_task.done():
|
||||
_LOGGER.debug(
|
||||
"Cancelled ongoing color adaptation calls (%s) for '%s'",
|
||||
color_task,
|
||||
|
|
@ -2269,7 +2332,11 @@ class AdaptiveLightingManager:
|
|||
"""Reset the 'manual_control' status of the lights."""
|
||||
for light in lights:
|
||||
if reset_manual_control:
|
||||
self.manual_control[light] = False
|
||||
_LOGGER.debug(
|
||||
"Light %s: Clearing manual control attributes.",
|
||||
light,
|
||||
)
|
||||
self.manual_control[light] = LightControlAttributes.NONE
|
||||
if timer := self.auto_reset_manual_control_timers.pop(light, None):
|
||||
timer.cancel()
|
||||
self.our_last_state_on_change.pop(light, None)
|
||||
|
|
@ -2319,11 +2386,29 @@ class AdaptiveLightingManager:
|
|||
self.turn_off_event[eid] = event
|
||||
self.reset(eid)
|
||||
|
||||
def on(eid: str, event: Event) -> None:
|
||||
async def on(eid: str, event: Event) -> None:
|
||||
task = self.sleep_tasks.get(eid)
|
||||
if task is not None:
|
||||
task.cancel()
|
||||
self.turn_on_event[eid] = event
|
||||
|
||||
try:
|
||||
switch = _switch_with_lights(
|
||||
self.hass,
|
||||
[eid],
|
||||
expand_light_groups=False,
|
||||
)
|
||||
await self.update_manually_controlled_from_event(
|
||||
switch,
|
||||
eid,
|
||||
force=False,
|
||||
)
|
||||
except NoSwitchFoundError:
|
||||
_LOGGER.debug(
|
||||
"No switch found for entity_id='%s' in 'on' event listener",
|
||||
eid,
|
||||
)
|
||||
|
||||
timer = self.auto_reset_manual_control_timers.get(eid)
|
||||
if (
|
||||
timer is not None
|
||||
|
|
@ -2351,7 +2436,7 @@ class AdaptiveLightingManager:
|
|||
event.context.id,
|
||||
)
|
||||
for eid in entity_ids:
|
||||
on(eid, event)
|
||||
await on(eid, event)
|
||||
|
||||
elif service == SERVICE_TOGGLE:
|
||||
_LOGGER.debug(
|
||||
|
|
@ -2366,7 +2451,7 @@ class AdaptiveLightingManager:
|
|||
if state.state == STATE_ON: # is turning off
|
||||
off(eid, event)
|
||||
elif state.state == STATE_OFF: # is turning on
|
||||
on(eid, event)
|
||||
await on(eid, event)
|
||||
|
||||
async def state_changed_event_listener(
|
||||
self,
|
||||
|
|
@ -2489,68 +2574,95 @@ class AdaptiveLightingManager:
|
|||
event,
|
||||
)
|
||||
|
||||
def is_manually_controlled(
|
||||
async def update_manually_controlled_from_event(
|
||||
self,
|
||||
switch: AdaptiveSwitch,
|
||||
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)
|
||||
if manual_control:
|
||||
# Manually controlled until light is turned on and off
|
||||
return True
|
||||
) -> None:
|
||||
"""Check if the light has been manually controlled by the latest turn on event."""
|
||||
if not switch._take_over_control:
|
||||
return
|
||||
|
||||
turn_on_event = self.turn_on_event.get(light)
|
||||
|
||||
if (
|
||||
turn_on_event is not None
|
||||
and not self.is_proactively_adapting(turn_on_event.context.id)
|
||||
and not is_our_context(turn_on_event.context)
|
||||
and not force
|
||||
turn_on_event is None
|
||||
or self.is_proactively_adapting(turn_on_event.context.id)
|
||||
or is_our_context(turn_on_event.context)
|
||||
or force
|
||||
):
|
||||
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))
|
||||
or (ATTR_FLASH in keys)
|
||||
or (ATTR_EFFECT in keys)
|
||||
):
|
||||
# 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)
|
||||
_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
|
||||
return
|
||||
|
||||
turn_on_attributes = get_light_control_attributes(
|
||||
turn_on_event.data[ATTR_SERVICE_DATA],
|
||||
)
|
||||
|
||||
if not turn_on_attributes:
|
||||
return
|
||||
|
||||
# Light was already on and 'light.turn_on' was not called by
|
||||
# the adaptive_lighting integration.
|
||||
self.add_manual_control_attributes(light, turn_on_attributes)
|
||||
switch.fire_manual_control_event(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 %s of the light until the switch or the"
|
||||
" light turns off and then on again.",
|
||||
light,
|
||||
turn_on_event.context.id,
|
||||
turn_on_attributes,
|
||||
)
|
||||
|
||||
async def update_manually_controlled_from_untracked_change(
|
||||
self,
|
||||
switch: AdaptiveSwitch,
|
||||
light: str,
|
||||
force: bool,
|
||||
context: Context,
|
||||
) -> None:
|
||||
"""Check if the light has been manually controlled from an untracked change.
|
||||
|
||||
An untracked change is a change that has been made outsideof HA and is
|
||||
therefore not visible through events.
|
||||
"""
|
||||
if not switch._take_over_control or not switch._detect_non_ha_changes or force:
|
||||
return
|
||||
|
||||
# Note: This call updates the state of the light
|
||||
# so it might suddenly be off.
|
||||
significantly_changed_attributes = await self.significant_change(
|
||||
switch,
|
||||
light,
|
||||
context,
|
||||
)
|
||||
|
||||
if not significantly_changed_attributes:
|
||||
return
|
||||
|
||||
self.add_manual_control_attributes(
|
||||
light,
|
||||
significantly_changed_attributes,
|
||||
)
|
||||
switch.fire_manual_control_event(light, context)
|
||||
|
||||
async def significant_change(
|
||||
self,
|
||||
switch: AdaptiveSwitch,
|
||||
light: str,
|
||||
adapt_brightness: bool,
|
||||
adapt_color: bool,
|
||||
context: Context, # just for logging
|
||||
) -> bool:
|
||||
) -> LightControlAttributes:
|
||||
"""Has the light made a significant change since last update.
|
||||
|
||||
This method will detect changes that were made to the light without
|
||||
calling 'light.turn_on', so outside of Home Assistant. If a change is
|
||||
detected, we mark the light as 'manually controlled' until the light
|
||||
or switch is turned 'off' and 'on' again.
|
||||
calling 'light.turn_on', so outside of Home Assistant.
|
||||
"""
|
||||
assert switch._detect_non_ha_changes
|
||||
|
||||
last_service_data = self.last_service_data.get(light)
|
||||
if last_service_data is None:
|
||||
return False
|
||||
return LightControlAttributes.NONE
|
||||
# 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
|
||||
|
|
@ -2559,33 +2671,32 @@ class AdaptiveLightingManager:
|
|||
refreshed_state = self.hass.states.get(light)
|
||||
assert refreshed_state is not None
|
||||
|
||||
changed = _attributes_have_changed(
|
||||
changed_attributes = _attributes_have_changed(
|
||||
old_attributes=last_service_data,
|
||||
new_attributes=refreshed_state.attributes,
|
||||
light=light,
|
||||
adapt_brightness=adapt_brightness,
|
||||
adapt_color=adapt_color,
|
||||
context=context,
|
||||
)
|
||||
if changed:
|
||||
if changed_attributes:
|
||||
_LOGGER.debug(
|
||||
"%s: State attributes of '%s' changed (%s) wrt 'last_service_data' (%s) (context.id=%s)",
|
||||
"%s: State attributes %s of '%s' changed (%s) wrt 'last_service_data' (%s) (context.id=%s)",
|
||||
switch._name,
|
||||
changed_attributes,
|
||||
light,
|
||||
refreshed_state.attributes,
|
||||
last_service_data,
|
||||
context.id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s: State attributes of '%s' did not change (%s) wrt 'last_service_data' (%s) (context.id=%s)",
|
||||
switch._name,
|
||||
light,
|
||||
refreshed_state.attributes,
|
||||
last_service_data,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
_LOGGER.debug(
|
||||
"%s: State attributes of '%s' did not change (%s) wrt 'last_service_data' (%s) (context.id=%s)",
|
||||
switch._name,
|
||||
light,
|
||||
refreshed_state.attributes,
|
||||
last_service_data,
|
||||
context.id,
|
||||
)
|
||||
return False
|
||||
return changed_attributes
|
||||
|
||||
def _off_to_on_state_event_is_from_turn_on(
|
||||
self,
|
||||
|
|
@ -2745,12 +2856,12 @@ class AdaptiveLightingManager:
|
|||
entity_id,
|
||||
service_data,
|
||||
)
|
||||
if any(
|
||||
attr in service_data
|
||||
for attr in COLOR_ATTRS | BRIGHTNESS_ATTRS | {ATTR_EFFECT}
|
||||
):
|
||||
self.mark_as_manual_control(entity_id)
|
||||
manual_control_attributes = get_light_control_attributes(service_data)
|
||||
|
||||
if manual_control_attributes:
|
||||
self.set_manual_control_attributes(entity_id, manual_control_attributes)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -53,8 +53,9 @@
|
|||
"brightness_mode": "brightness_mode",
|
||||
"brightness_mode_time_dark": "brightness_mode_time_dark",
|
||||
"brightness_mode_time_light": "brightness_mode_time_light",
|
||||
"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`! 🔒",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
|
||||
"take_over_control": "take_over_control: Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒",
|
||||
"take_over_control_mode": "take_over_control_mode",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.",
|
||||
"autoreset_control_seconds": "autoreset_control_seconds",
|
||||
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
|
||||
"adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️",
|
||||
|
|
@ -86,6 +87,7 @@
|
|||
"brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
|
||||
"brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
|
||||
"brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
|
||||
"take_over_control_mode": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.",
|
||||
"autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
|
||||
"send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
|
||||
"adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️"
|
||||
|
|
@ -145,7 +147,7 @@
|
|||
"name": "lights"
|
||||
},
|
||||
"manual_control": {
|
||||
"description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒",
|
||||
"description": "Whether to add (\"true\") or remove (\"false\") all adapted attributes of the light from the \"manual_control\" list, or the name of an attribute for selective addition. 🔒",
|
||||
"name": "manual_control"
|
||||
}
|
||||
}
|
||||
|
|
@ -251,11 +253,15 @@
|
|||
"name": "min_sunset_time"
|
||||
},
|
||||
"take_over_control": {
|
||||
"description": "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`! 🔒",
|
||||
"description": "Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. 🔒",
|
||||
"name": "take_over_control"
|
||||
},
|
||||
"take_over_control_mode": {
|
||||
"description": "The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed.",
|
||||
"name": "take_over_control_mode"
|
||||
},
|
||||
"detect_non_ha_changes": {
|
||||
"description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
|
||||
"description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.",
|
||||
"name": "detect_non_ha_changes"
|
||||
},
|
||||
"transition": {
|
||||
|
|
|
|||
0
scripts/develop
Normal file → Executable file
0
scripts/develop
Normal file → Executable file
0
scripts/lint
Normal file → Executable file
0
scripts/lint
Normal file → Executable file
|
|
@ -4,16 +4,26 @@ from unittest.mock import Mock
|
|||
|
||||
import pytest
|
||||
from homeassistant.components.adaptive_lighting.adaptation_utils import (
|
||||
LightControlAttributes,
|
||||
ServiceData,
|
||||
_create_service_call_data_iterator,
|
||||
_has_relevant_service_data_attributes,
|
||||
_remove_redundant_attributes,
|
||||
_split_service_call_data,
|
||||
get_light_control_attributes,
|
||||
has_brightness_attribute,
|
||||
has_color_attribute,
|
||||
has_effect_attribute,
|
||||
manual_control_event_attribute_to_flags,
|
||||
prepare_adaptation_data,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_EFFECT,
|
||||
ATTR_FLASH,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
|
||||
|
|
@ -338,3 +348,135 @@ def fixture_hass_states_mock():
|
|||
hass = Mock()
|
||||
hass.states.get.return_value = Mock(attributes={ATTR_BRIGHTNESS: 10})
|
||||
return hass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("attribute", "expected_str", "has_any", "has_none", "has_all"),
|
||||
[
|
||||
(LightControlAttributes.NONE, "NONE", False, True, False),
|
||||
(LightControlAttributes.BRIGHTNESS, "BRIGHTNESS", True, False, False),
|
||||
(LightControlAttributes.COLOR, "COLOR", True, False, False),
|
||||
(
|
||||
LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR,
|
||||
"BRIGHTNESS|COLOR",
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
(
|
||||
LightControlAttributes.ALL,
|
||||
"BRIGHTNESS|COLOR",
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_light_control_attribute_flags(
|
||||
attribute: LightControlAttributes,
|
||||
expected_str: str,
|
||||
has_any: bool,
|
||||
has_none: bool,
|
||||
has_all: bool,
|
||||
):
|
||||
"""Test helper methods and string conversion for the light attribute flag."""
|
||||
assert str(attribute) == expected_str
|
||||
assert attribute.has_any() is has_any
|
||||
assert attribute.has_none() is has_none
|
||||
assert attribute.has_all() is has_all
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("manual_control_attribute", "expected_flag"),
|
||||
[
|
||||
(True, LightControlAttributes.ALL),
|
||||
(False, LightControlAttributes.NONE),
|
||||
("brightness", LightControlAttributes.BRIGHTNESS),
|
||||
("color", LightControlAttributes.COLOR),
|
||||
("unsupported", LightControlAttributes.NONE),
|
||||
],
|
||||
)
|
||||
def test_manual_control_event_attribute_to_flags(
|
||||
manual_control_attribute: bool | str,
|
||||
expected_flag: LightControlAttributes,
|
||||
):
|
||||
"""Test mapping of manual control events to attribute flags."""
|
||||
assert (
|
||||
manual_control_event_attribute_to_flags(manual_control_attribute)
|
||||
== expected_flag
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "expected"),
|
||||
[
|
||||
({ATTR_BRIGHTNESS: 125}, True),
|
||||
({ATTR_BRIGHTNESS_PCT: 50}, True),
|
||||
({ATTR_BRIGHTNESS_PCT: 50, ATTR_COLOR_TEMP_KELVIN: 3500}, True),
|
||||
({ATTR_BRIGHTNESS_PCT: 50, "unknown": "foo"}, True),
|
||||
({ATTR_COLOR_TEMP_KELVIN: 3500}, False),
|
||||
({}, False),
|
||||
],
|
||||
)
|
||||
def test_has_brightness_attribute(service_data: ServiceData, expected: bool):
|
||||
"""Test detection of brightness attributes in service data."""
|
||||
assert has_brightness_attribute(service_data) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "expected"),
|
||||
[
|
||||
({ATTR_HS_COLOR: (10, 20)}, True),
|
||||
({ATTR_COLOR_TEMP_KELVIN: 5000}, True),
|
||||
({ATTR_COLOR_TEMP_KELVIN: 5000, ATTR_BRIGHTNESS: 125}, True),
|
||||
({ATTR_COLOR_TEMP_KELVIN: 5000, "unknown": "foo"}, True),
|
||||
({ATTR_BRIGHTNESS: 125}, False),
|
||||
({}, False),
|
||||
],
|
||||
)
|
||||
def test_has_color_attribute(service_data: ServiceData, expected: bool):
|
||||
"""Test detection of color attributes in service data."""
|
||||
assert has_color_attribute(service_data) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "expected"),
|
||||
[
|
||||
({ATTR_EFFECT: "colorloop"}, True),
|
||||
({ATTR_FLASH: "short"}, True),
|
||||
({ATTR_EFFECT: "colorloop", ATTR_FLASH: "short"}, True),
|
||||
({ATTR_EFFECT: "colorloop", "unknown": "foo"}, True),
|
||||
({}, False),
|
||||
],
|
||||
)
|
||||
def test_has_effect_attribute(service_data: ServiceData, expected: bool):
|
||||
"""Test detection of effect attributes in service data."""
|
||||
assert has_effect_attribute(service_data) is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "expected_flags"),
|
||||
[
|
||||
({ATTR_BRIGHTNESS: 1}, LightControlAttributes.BRIGHTNESS),
|
||||
({ATTR_HS_COLOR: (1, 2)}, LightControlAttributes.COLOR),
|
||||
(
|
||||
{ATTR_BRIGHTNESS: 1, ATTR_HS_COLOR: (1, 2)},
|
||||
LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR,
|
||||
),
|
||||
(
|
||||
{ATTR_EFFECT: "colorloop"},
|
||||
LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR,
|
||||
),
|
||||
(
|
||||
{ATTR_FLASH: "short"},
|
||||
LightControlAttributes.BRIGHTNESS | LightControlAttributes.COLOR,
|
||||
),
|
||||
({}, LightControlAttributes.NONE),
|
||||
],
|
||||
)
|
||||
def test_get_light_control_attributes(
|
||||
service_data: ServiceData,
|
||||
expected_flags: LightControlAttributes,
|
||||
):
|
||||
"""Test determination of light control attributes."""
|
||||
assert get_light_control_attributes(service_data) == expected_flags
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import voluptuous.error
|
|||
from flaky import flaky
|
||||
from homeassistant.components.adaptive_lighting.adaptation_utils import (
|
||||
AdaptationData,
|
||||
LightControlAttributes,
|
||||
_create_service_call_data_iterator,
|
||||
)
|
||||
from homeassistant.components.adaptive_lighting.color_and_brightness import (
|
||||
|
|
@ -60,6 +61,7 @@ from homeassistant.components.adaptive_lighting.const import (
|
|||
SERVICE_SET_MANUAL_CONTROL,
|
||||
SLEEP_MODE_SWITCH,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
TakeOverControlMode,
|
||||
)
|
||||
from homeassistant.components.adaptive_lighting.switch import (
|
||||
CONF_INTERCEPT,
|
||||
|
|
@ -690,7 +692,7 @@ async def test_manual_control(
|
|||
# Call light.turn_on for ENTITY_LIGHT_1
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
# Check that ENTITY_LIGHT_1 is manually controlled
|
||||
assert manual_control[ENTITY_LIGHT_1]
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS
|
||||
# Test adaptive_lighting.set_manual_control
|
||||
await change_manual_control(False)
|
||||
# Check that ENTITY_LIGHT_1 is not manually controlled
|
||||
|
|
@ -703,11 +705,9 @@ async def test_manual_control(
|
|||
assert not manual_control[ENTITY_LIGHT_1], manual_control
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
|
||||
if adapt_only_on_bare_turn_on:
|
||||
# Marks as manually controlled beacuse we turned it on with brightness
|
||||
assert manual_control[ENTITY_LIGHT_1], manual_control
|
||||
else:
|
||||
assert not manual_control[ENTITY_LIGHT_1], manual_control
|
||||
assert (
|
||||
manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS
|
||||
), manual_control
|
||||
|
||||
# Check that toggling (sleep mode) switch resets manual control
|
||||
for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]:
|
||||
|
|
@ -722,7 +722,7 @@ async def test_manual_control(
|
|||
await turn_light(False)
|
||||
await change_manual_control(True)
|
||||
await turn_light(True)
|
||||
assert manual_control[ENTITY_LIGHT_1]
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
|
||||
|
||||
# Check that when 'adapt_brightness' is off, changing the brightness
|
||||
# doesn't mark it as manually controlled but changing color_temp
|
||||
|
|
@ -732,7 +732,7 @@ async def test_manual_control(
|
|||
assert not manual_control[ENTITY_LIGHT_1]
|
||||
await switch.adapt_brightness_switch.async_turn_off()
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
assert not manual_control[ENTITY_LIGHT_1]
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS
|
||||
mired_range = (light.min_color_temp_kelvin, light.max_color_temp_kelvin)
|
||||
kelvin_range = (
|
||||
color_temperature_mired_to_kelvin(mired_range[1]),
|
||||
|
|
@ -743,7 +743,7 @@ async def test_manual_control(
|
|||
True,
|
||||
color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin,
|
||||
)
|
||||
assert manual_control[ENTITY_LIGHT_1]
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
|
||||
await switch.adapt_brightness_switch.async_turn_on() # turn on again
|
||||
|
||||
# Check that when 'adapt_color' is off, changing the color
|
||||
|
|
@ -753,10 +753,10 @@ async def test_manual_control(
|
|||
await turn_light(True)
|
||||
assert not manual_control[ENTITY_LIGHT_1]
|
||||
await switch.adapt_color_switch.async_turn_off()
|
||||
await turn_light(True, color_temp=increased_color_temp())
|
||||
assert not manual_control[ENTITY_LIGHT_1]
|
||||
await turn_light(True, color_temp_kelvin=increased_color_temp())
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.COLOR
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
assert manual_control[ENTITY_LIGHT_1]
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
|
||||
|
||||
# Check that when 'adapt_color' adapt_brightness are both off
|
||||
# nothing marks it as manually controlled
|
||||
|
|
@ -765,14 +765,14 @@ async def test_manual_control(
|
|||
await switch.adapt_color_switch.async_turn_off()
|
||||
await switch.adapt_brightness_switch.async_turn_off()
|
||||
assert not manual_control[ENTITY_LIGHT_1]
|
||||
await turn_light(True, color_temp=increased_color_temp())
|
||||
await turn_light(True, color_temp_kelvin=increased_color_temp())
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
await turn_light(
|
||||
True,
|
||||
color_temp=increased_color_temp(),
|
||||
color_temp_kelvin=increased_color_temp(),
|
||||
brightness=increased_brightness(),
|
||||
)
|
||||
assert not manual_control[ENTITY_LIGHT_1]
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
|
||||
# Turn switches on again
|
||||
await switch.adapt_color_switch.async_turn_on()
|
||||
await switch.adapt_brightness_switch.async_turn_on()
|
||||
|
|
@ -800,6 +800,22 @@ async def test_manual_control(
|
|||
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
|
||||
assert not manual_control[ENTITY_LIGHT_1]
|
||||
|
||||
# Check that manual control `True` sets all attributes
|
||||
await change_manual_control(False)
|
||||
assert not manual_control[ENTITY_LIGHT_1]
|
||||
await change_manual_control(True)
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
|
||||
|
||||
# Check that manual control `False` unsets all attributes
|
||||
await change_manual_control(False)
|
||||
assert not manual_control[ENTITY_LIGHT_1]
|
||||
|
||||
# Check that manual control attributes can be selectively set
|
||||
await change_manual_control("brightness")
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS
|
||||
await change_manual_control("color")
|
||||
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.COLOR
|
||||
|
||||
|
||||
@flaky(max_runs=3, min_passes=1)
|
||||
async def test_auto_reset_manual_control(hass):
|
||||
|
|
@ -833,7 +849,7 @@ async def test_auto_reset_manual_control(hass):
|
|||
_LOGGER.debug("Start test auto reset manual control")
|
||||
await turn_light(True, brightness=1)
|
||||
await turn_light(True, brightness=10)
|
||||
assert manual_control[light.entity_id]
|
||||
assert manual_control[light.entity_id] == LightControlAttributes.BRIGHTNESS
|
||||
assert (
|
||||
switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0
|
||||
)
|
||||
|
|
@ -856,6 +872,143 @@ async def test_auto_reset_manual_control(hass):
|
|||
assert not manual_control[light.entity_id]
|
||||
|
||||
|
||||
async def test_adaptation_attribute_selection(hass):
|
||||
"""Test the 'manual control' tracking."""
|
||||
switch, (light, *_) = await setup_lights_and_switch(hass)
|
||||
|
||||
# Assert default settings
|
||||
assert switch._take_over_control
|
||||
assert switch._take_over_control_mode == TakeOverControlMode.PAUSE_ALL
|
||||
|
||||
# Check that PAUSE_ALL leads to adaptation of all attributes when none are manually controlled
|
||||
assert (
|
||||
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.NONE
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.ALL
|
||||
)
|
||||
|
||||
# Check that PAUSE_ALL leads to no adaptation when a single attribute is manually controlled
|
||||
switch.manager.add_manual_control_attributes(
|
||||
ENTITY_LIGHT_1,
|
||||
LightControlAttributes.BRIGHTNESS,
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.BRIGHTNESS
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.NONE
|
||||
)
|
||||
|
||||
# Check that PAUSE_ALL leads to no adaptation when all attributes are manually controlled
|
||||
switch.manager.add_manual_control_attributes(
|
||||
ENTITY_LIGHT_1,
|
||||
LightControlAttributes.COLOR,
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.ALL
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.NONE
|
||||
)
|
||||
|
||||
switch._take_over_control_mode = TakeOverControlMode.PAUSE_CHANGED
|
||||
switch.manager.set_manual_control_attributes(
|
||||
ENTITY_LIGHT_1,
|
||||
LightControlAttributes.NONE,
|
||||
)
|
||||
|
||||
# Check that PAUSE_CHANGED leads to adaptation of all attributes when none are manually controlled
|
||||
assert (
|
||||
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.NONE
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.ALL
|
||||
)
|
||||
|
||||
# Check that PAUSE_CHANGED leads to adaptation of the remaining non-manual attributes
|
||||
switch.manager.add_manual_control_attributes(
|
||||
ENTITY_LIGHT_1,
|
||||
LightControlAttributes.BRIGHTNESS,
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.BRIGHTNESS
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.COLOR
|
||||
)
|
||||
|
||||
# Check that PAUSE_CHANGED leads to no adaptation when all attributes are manually controlled
|
||||
switch.manager.add_manual_control_attributes(
|
||||
ENTITY_LIGHT_1,
|
||||
LightControlAttributes.COLOR,
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.ALL
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.NONE
|
||||
)
|
||||
|
||||
await switch.adapt_brightness_switch.async_turn_off()
|
||||
|
||||
# Check that with adapt_brightness off and PAUSE_CHANGED, only color is adapted when none are manually controlled
|
||||
switch._take_over_control_mode = TakeOverControlMode.PAUSE_CHANGED
|
||||
switch.manager.set_manual_control_attributes(
|
||||
ENTITY_LIGHT_1,
|
||||
LightControlAttributes.NONE,
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.COLOR
|
||||
)
|
||||
|
||||
# Check that with adapt_brightness off and PAUSE_CHANGED, nothing is adapted when color is manually controlled
|
||||
switch._take_over_control_mode = TakeOverControlMode.PAUSE_CHANGED
|
||||
switch.manager.set_manual_control_attributes(
|
||||
ENTITY_LIGHT_1,
|
||||
LightControlAttributes.COLOR,
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.NONE
|
||||
)
|
||||
|
||||
# Check that with adapt_brightness off and PAUSE_ALL, only color is adapted when none are manually controlled
|
||||
switch._take_over_control_mode = TakeOverControlMode.PAUSE_ALL
|
||||
switch.manager.set_manual_control_attributes(
|
||||
ENTITY_LIGHT_1,
|
||||
LightControlAttributes.NONE,
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.COLOR
|
||||
)
|
||||
|
||||
# Check that with adapt_brightness off and PAUSE_ALL, nothing is adapted when color is manually controlled
|
||||
switch._take_over_control_mode = TakeOverControlMode.PAUSE_ALL
|
||||
switch.manager.set_manual_control_attributes(
|
||||
ENTITY_LIGHT_1,
|
||||
LightControlAttributes.COLOR,
|
||||
)
|
||||
assert (
|
||||
switch.manager.get_adaption_control_attributes(switch, ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.NONE
|
||||
)
|
||||
|
||||
|
||||
async def test_apply_service(hass):
|
||||
"""Test adaptive_lighting.apply service."""
|
||||
switch, (_, _, light) = await setup_lights_and_switch(hass)
|
||||
|
|
@ -994,8 +1147,6 @@ def test_attributes_have_changed():
|
|||
}
|
||||
kwargs = {
|
||||
"light": "light.test",
|
||||
"adapt_brightness": True,
|
||||
"adapt_color": True,
|
||||
"context": Context(),
|
||||
}
|
||||
assert not _attributes_have_changed(
|
||||
|
|
@ -1229,9 +1380,15 @@ async def test_state_change_handlers(hass):
|
|||
await turn_light(True, brightness=40)
|
||||
await turn_light(True, brightness=20)
|
||||
await update(force=False)
|
||||
assert switch.manager.manual_control[ENTITY_LIGHT_1]
|
||||
assert (
|
||||
switch.manager.manual_control[ENTITY_LIGHT_1]
|
||||
== LightControlAttributes.BRIGHTNESS
|
||||
)
|
||||
await update(force=True)
|
||||
assert switch.manager.manual_control[ENTITY_LIGHT_1]
|
||||
assert (
|
||||
switch.manager.manual_control[ENTITY_LIGHT_1]
|
||||
== LightControlAttributes.BRIGHTNESS
|
||||
)
|
||||
|
||||
# turn light off then on should reset manual control.
|
||||
await turn_light(False)
|
||||
|
|
@ -1245,7 +1402,10 @@ async def test_state_change_handlers(hass):
|
|||
await update(force=False)
|
||||
assert switch.manager.last_service_data.get(ENTITY_LIGHT_1) is not None
|
||||
assert switch.manager.our_last_state_on_change.get(ENTITY_LIGHT_1) is not None
|
||||
assert switch.manager.manual_control[ENTITY_LIGHT_1]
|
||||
assert (
|
||||
switch.manager.manual_control[ENTITY_LIGHT_1]
|
||||
== LightControlAttributes.BRIGHTNESS
|
||||
)
|
||||
|
||||
|
||||
def test_is_our_context():
|
||||
|
|
@ -1345,7 +1505,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands):
|
|||
# check whether the brightness and color_temp change.
|
||||
context = switch.create_context("test") # needs to be passed to update method
|
||||
brightness = light.brightness
|
||||
color_temp = light.color_temp
|
||||
color_temp = light.color_temp_kelvin
|
||||
await switch.sleep_mode_switch.async_turn_on()
|
||||
await switch._update_attrs_and_maybe_adapt_lights(context=context)
|
||||
await hass.async_block_till_done()
|
||||
|
|
@ -1353,7 +1513,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands):
|
|||
# TODO: figure out why `light.brightness` is not updating
|
||||
attrs = hass.states.get(light.entity_id).attributes
|
||||
sleep_brightness = attrs["brightness"]
|
||||
sleep_color_temp = attrs["color_temp"]
|
||||
sleep_color_temp = attrs["color_temp_kelvin"]
|
||||
|
||||
assert sleep_brightness != brightness
|
||||
assert sleep_color_temp != color_temp
|
||||
|
|
@ -1364,7 +1524,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands):
|
|||
|
||||
attrs = hass.states.get(light.entity_id).attributes
|
||||
brightness = attrs["brightness"]
|
||||
color_temp = attrs["color_temp"]
|
||||
color_temp = attrs["color_temp_kelvin"]
|
||||
|
||||
assert sleep_brightness != brightness
|
||||
assert sleep_color_temp != color_temp
|
||||
|
|
@ -1538,7 +1698,7 @@ async def test_cancellable_service_calls_task(hass):
|
|||
_create_service_call_data_iterator(hass, [service_data], False),
|
||||
force=False,
|
||||
max_length=1,
|
||||
which="both",
|
||||
attributes=LightControlAttributes.ALL,
|
||||
)
|
||||
await switch.execute_cancellable_adaptation_calls(adaptation_data)
|
||||
|
||||
|
|
@ -1948,7 +2108,7 @@ async def test_two_switches_for_single_light(hass):
|
|||
|
||||
assert light1.is_on
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
await turn_light(True, color_temp=increased_color_temp())
|
||||
await turn_light(True, color_temp_kelvin=increased_color_temp())
|
||||
|
||||
attrs = hass.states.get(light1.entity_id).attributes
|
||||
before_brightness = attrs[ATTR_BRIGHTNESS]
|
||||
|
|
@ -2381,12 +2541,9 @@ def test_attributes_have_changed_light_mode_switch():
|
|||
context = Context()
|
||||
base_kwargs = {
|
||||
"light": "light.test",
|
||||
"adapt_brightness": True,
|
||||
"context": context,
|
||||
}
|
||||
|
||||
# Test 1: adapt_color=True - all mode changes should be detected
|
||||
kwargs_adapt_color = {**base_kwargs, "adapt_color": True}
|
||||
kwargs_adapt_color = base_kwargs
|
||||
|
||||
# color_temp → RGB
|
||||
assert _attributes_have_changed(
|
||||
|
|
@ -2449,21 +2606,6 @@ def test_attributes_have_changed_light_mode_switch():
|
|||
**kwargs_adapt_color,
|
||||
), "Same XY should not be detected as change"
|
||||
|
||||
# Test 2: adapt_color=False - mode changes should NOT be detected
|
||||
kwargs_no_adapt = {**base_kwargs, "adapt_color": False}
|
||||
|
||||
assert not _attributes_have_changed(
|
||||
old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000},
|
||||
new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)},
|
||||
**kwargs_no_adapt,
|
||||
), "Mode change should not be detected when adapt_color=False"
|
||||
|
||||
assert not _attributes_have_changed(
|
||||
old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)},
|
||||
new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000},
|
||||
**kwargs_no_adapt,
|
||||
), "RGB → color_temp should not be detected when adapt_color=False"
|
||||
|
||||
|
||||
# Regression tests for bugs found in PR #1348 by @protyposis
|
||||
# See: https://github.com/basnijholt/adaptive-lighting/pull/1348
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue