mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-28 21:04:19 +02:00
Implement call intercept for multiple lights (#679)
* Implement call intercept for multiple lights * remove comment * skip if no eids * add comment * fix type * Fix skipped * indentation * Add logging and fix error * Fix for HA ≤2023.04 * simplify * remove unused ignores * Debug mode * Add test * Make test failing * rename switch * rename lights * Fix tests * Rename lights in tests * Remove unused dependencies * Improve tests * More tests * Remove the DEBUG_MODE * Add doc-string * Extra test * assert * extra test * Comments * fix * fix * expand light groups * more logging * sort * Revert is_proactively_adapting checks This reverts commit39fd8f2be0. * simplify the mapping * Revert "Revert is_proactively_adapting checks" This reverts commit18803e8e50. * test * no light groups * do not expand * Do not expand_light_groups in intercept * more logging * Fix * add comment * Add multi_light_intercept config option * Update README.md, strings.json, and services.yaml * add light group * fix platform * add simple test * turn off again * Test without take over control * improve test and fix it in one way * Fixes * add cleanup fixture * format * Update test_switch.py * add __str__ * remove unneeded call * simplify service_data construction * Generalize is_our_context * Fix multi_light_intercept: false * add comments * add docs * Update README.md, strings.json, and services.yaml * Add feature line * move function --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
4251ccacc5
commit
1ef7ed507e
8 changed files with 646 additions and 120 deletions
|
|
@ -150,6 +150,20 @@ class AdaptationData:
|
|||
"""Return data for the next service call, or none if no more data exists."""
|
||||
return await anext(self.service_call_datas, None)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the data."""
|
||||
return (
|
||||
f"{self.__class__.__name__}("
|
||||
f"entity_id={self.entity_id}, "
|
||||
f"context_id={self.context.id}, "
|
||||
f"sleep_time={self.sleep_time}, "
|
||||
f"force={self.force}, "
|
||||
f"max_length={self.max_length}, "
|
||||
f"which={self.which}, "
|
||||
f"initial_sleep={self.initial_sleep}"
|
||||
")"
|
||||
)
|
||||
|
||||
|
||||
class NoColorOrBrightnessInServiceDataError(Exception):
|
||||
"""Exception raised when no color or brightness attributes are found in service data."""
|
||||
|
|
|
|||
|
|
@ -186,10 +186,20 @@ CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = (
|
|||
DOCS[CONF_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. "
|
||||
"adaptation responsivity in some situations. 📉"
|
||||
"Disable if physical light states get out of sync with HA's recorded state."
|
||||
)
|
||||
|
||||
CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = (
|
||||
"multi_light_intercept",
|
||||
True,
|
||||
)
|
||||
DOCS[CONF_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."
|
||||
)
|
||||
|
||||
SLEEP_MODE_SWITCH = "sleep_mode_switch"
|
||||
ADAPT_COLOR_SWITCH = "adapt_color_switch"
|
||||
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
|
||||
|
|
@ -290,6 +300,7 @@ VALIDATION_TUPLES = [
|
|||
DEFAULT_SKIP_REDUNDANT_COMMANDS,
|
||||
bool,
|
||||
),
|
||||
(CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@
|
|||
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
|
||||
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
|
||||
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
|
||||
"skip_redundant_commands": "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."
|
||||
"skip_redundant_commands": "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.",
|
||||
"multi_light_intercept": "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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import ulid_transform
|
|||
import voluptuous as vol
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_EFFECT,
|
||||
ATTR_FLASH,
|
||||
|
|
@ -119,6 +120,7 @@ from .const import (
|
|||
CONF_MIN_BRIGHTNESS,
|
||||
CONF_MIN_COLOR_TEMP,
|
||||
CONF_MIN_SUNSET_TIME,
|
||||
CONF_MULTI_LIGHT_INTERCEPT,
|
||||
CONF_ONLY_ONCE,
|
||||
CONF_PREFER_RGB_COLOR,
|
||||
CONF_SEND_SPLIT_DELAY,
|
||||
|
|
@ -265,30 +267,39 @@ def create_context(
|
|||
return Context(id=context_id, parent_id=parent_id)
|
||||
|
||||
|
||||
def is_our_context_id(context_id: str | None) -> bool:
|
||||
def is_our_context_id(context_id: str | None, which: str | None = None) -> bool:
|
||||
"""Check whether this integration created 'context_id'."""
|
||||
if context_id is None:
|
||||
return False
|
||||
return f":{_DOMAIN_SHORT}:" in context_id
|
||||
|
||||
is_al = f":{_DOMAIN_SHORT}:" in context_id
|
||||
if not is_al:
|
||||
return False
|
||||
if which is None:
|
||||
return True
|
||||
return f":{_remove_vowels(which)}:" in context_id
|
||||
|
||||
|
||||
def is_our_context(context: Context | None) -> bool:
|
||||
def is_our_context(context: Context | None, which: str | None = None) -> bool:
|
||||
"""Check whether this integration created 'context'."""
|
||||
if context is None:
|
||||
return False
|
||||
return is_our_context_id(context.id)
|
||||
return is_our_context_id(context.id, which)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def _switches_with_lights(
|
||||
hass: HomeAssistant,
|
||||
lights: list[str],
|
||||
expand_light_groups: bool = True,
|
||||
) -> list[AdaptiveSwitch]:
|
||||
"""Get all switches that control at least one of the lights passed."""
|
||||
config_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
data = hass.data[DOMAIN]
|
||||
switches = []
|
||||
all_check_lights = _expand_light_groups(hass, lights)
|
||||
all_check_lights = (
|
||||
_expand_light_groups(hass, lights) if expand_light_groups else set(lights)
|
||||
)
|
||||
for config in config_entries:
|
||||
entry = data.get(config.entry_id)
|
||||
if entry is None: # entry might be disabled and therefore missing
|
||||
|
|
@ -309,9 +320,10 @@ class NoSwitchFoundError(ValueError):
|
|||
def _switch_with_lights(
|
||||
hass: HomeAssistant,
|
||||
lights: list[str],
|
||||
expand_light_groups: bool = True,
|
||||
) -> AdaptiveSwitch:
|
||||
"""Find the switch that controls the lights in 'lights'."""
|
||||
switches = _switches_with_lights(hass, lights)
|
||||
switches = _switches_with_lights(hass, lights, expand_light_groups)
|
||||
if len(switches) == 1:
|
||||
return switches[0]
|
||||
if len(switches) > 1:
|
||||
|
|
@ -643,7 +655,10 @@ def _is_state_event(event: Event, from_or_to_state: Iterable[str]):
|
|||
|
||||
|
||||
@bind_hass
|
||||
def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
|
||||
def _expand_light_groups(
|
||||
hass: HomeAssistant,
|
||||
lights: list[str],
|
||||
) -> list[str]:
|
||||
all_lights = set()
|
||||
manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER]
|
||||
for light in lights:
|
||||
|
|
@ -651,14 +666,18 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
|
|||
if state is None:
|
||||
_LOGGER.debug("State of %s is None", light)
|
||||
all_lights.add(light)
|
||||
elif "entity_id" in state.attributes: # it's a light group
|
||||
elif _is_light_group(state):
|
||||
group = state.attributes["entity_id"]
|
||||
manager.lights.discard(light)
|
||||
all_lights.update(group)
|
||||
_LOGGER.debug("Expanded %s to %s", light, group)
|
||||
else:
|
||||
all_lights.add(light)
|
||||
return list(all_lights)
|
||||
return sorted(all_lights)
|
||||
|
||||
|
||||
def _is_light_group(state: State) -> bool:
|
||||
return "entity_id" in state.attributes
|
||||
|
||||
|
||||
@bind_hass
|
||||
|
|
@ -927,6 +946,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self._take_over_control = True
|
||||
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
|
||||
self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS]
|
||||
self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT]
|
||||
self._expand_light_groups() # updates manual control timers
|
||||
location, _ = get_astral_location(self.hass)
|
||||
|
||||
|
|
@ -1272,11 +1292,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
prefer_rgb_color: bool | None = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
# This should never happen if it's been proactively adapted.
|
||||
# The context.parent_id is the context.id of the service call that was intercepted
|
||||
# and context.id here is from the resulting "light_event" event.
|
||||
assert not self.manager.is_proactively_adapting(context.parent_id)
|
||||
|
||||
if (lock := self.manager.turn_off_locks.get(light)) and lock.locked():
|
||||
_LOGGER.debug("%s: '%s' is locked", self._name, light)
|
||||
return
|
||||
|
|
@ -1874,6 +1889,9 @@ class AdaptiveLightingManager:
|
|||
# Track light transitions
|
||||
self.transition_timers: dict[str, _AsyncSingleShotTimer] = {}
|
||||
|
||||
# Track _execute_cancellable_adaptation_calls tasks
|
||||
self.adaptation_tasks = set()
|
||||
|
||||
# Setup listeners and its callbacks to remove them later
|
||||
self.listener_removers = [
|
||||
self.hass.bus.async_listen(
|
||||
|
|
@ -1958,65 +1976,235 @@ class AdaptiveLightingManager:
|
|||
for key in keys:
|
||||
self._proactively_adapting_contexts.pop(key)
|
||||
|
||||
async def _service_interceptor_turn_on_handler( # noqa: PLR0911
|
||||
async def _service_interceptor_turn_on_handler( # noqa: PLR0912, PLR0915
|
||||
self,
|
||||
call: ServiceCall,
|
||||
data: ServiceData,
|
||||
):
|
||||
# Don't adapt our own service calls
|
||||
if is_our_context(call.context):
|
||||
) -> None:
|
||||
"""Intercept `light.turn_on` and `light.toggle` service calls and adapt them.
|
||||
|
||||
It is possible that the calls are made for multiple lights at once,
|
||||
which in turn might be in different switches or no switches at all.
|
||||
If there are lights that are not all in a single switch, we need to
|
||||
make multiple calls to `light.turn_on` with the correct entity IDs.
|
||||
One of these calls can be intercepted and adapted, the others need to
|
||||
be adapted by calling `_adapt_light` with the correct entity IDs or
|
||||
by calling `light.turn_on` directly.
|
||||
|
||||
We create a mapping from switch to entity IDs and keep a list
|
||||
of skipped lights which are lights in no switches or in switches that
|
||||
are off or lights that are already on.
|
||||
|
||||
If there is only one switch and 0 skipped lights, we just intercept the
|
||||
call directly.
|
||||
|
||||
If there are multiple switches and skipped lights, we can adapt the call
|
||||
for one of the switches to include only the lights in that switch and
|
||||
need to call `_adapt_light` for the other switches with their
|
||||
entity_ids. For skipped lights, we call light.turn_on directly with the
|
||||
entity_ids and original service data.
|
||||
|
||||
If there are only skipped lights, we can use the intercepted call
|
||||
directly.
|
||||
"""
|
||||
is_skipped_hash = is_our_context(call.context, "skipped")
|
||||
_LOGGER.debug(
|
||||
"(0) _service_interceptor_turn_on_handler: call.context.id='%s', is_skipped_hash='%s'",
|
||||
call.context.id,
|
||||
is_skipped_hash,
|
||||
)
|
||||
if is_our_context(call.context) and not is_skipped_hash:
|
||||
# Don't adapt our own service calls, but do re-adapt calls that
|
||||
# were skipped by us
|
||||
return
|
||||
|
||||
if ATTR_EFFECT in data[CONF_PARAMS] or ATTR_FLASH in data[CONF_PARAMS]:
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"(1) _service_interceptor_turn_on_handler: call='%s', data='%s'",
|
||||
call,
|
||||
data,
|
||||
)
|
||||
|
||||
entity_ids = self._get_entity_list(data)
|
||||
# Note: we do not expand light groups anywhere in this method, instead
|
||||
# we skip them and rely on the followup call that HA will make
|
||||
# with the expanded entity IDs.
|
||||
|
||||
# For simplicity, only service calls affecting a single entity are currently handled.
|
||||
#
|
||||
# To add support for adapting multiple entities, the following properties
|
||||
# need to hold for _all_ entities:
|
||||
# - managed by this AL instance
|
||||
# - not manually controlled
|
||||
# - supporting the same relevant feature set
|
||||
# - off state
|
||||
if len(entity_ids) != 1:
|
||||
return
|
||||
# Create a mapping from switch to entity IDs
|
||||
# AdaptiveSwitch.name → entity_ids mapping
|
||||
switch_to_eids: dict[str, list[str]] = {}
|
||||
# AdaptiveSwitch.name → AdaptiveSwitch mapping
|
||||
switch_name_mapping: dict[str, AdaptiveSwitch] = {}
|
||||
# Note: In HA≥2023.5, AdaptiveSwitch is hashable, so we can
|
||||
# use dict[AdaptiveSwitch, list[str]]
|
||||
skipped: list[str] = []
|
||||
for entity_id in entity_ids:
|
||||
try:
|
||||
switch = _switch_with_lights(
|
||||
self.hass,
|
||||
[entity_id],
|
||||
# Do not expand light groups, because HA will make a separate light.turn_on
|
||||
# call where the lights are expanded, and that call will be intercepted.
|
||||
expand_light_groups=False,
|
||||
)
|
||||
except NoSwitchFoundError:
|
||||
# Needs to make the original call but without adaptation
|
||||
skipped.append(entity_id)
|
||||
_LOGGER.debug(
|
||||
"No switch found for entity_id='%s', skipped='%s'",
|
||||
entity_id,
|
||||
skipped,
|
||||
)
|
||||
else:
|
||||
if (
|
||||
not switch.is_on
|
||||
# Never adapt on light groups, because HA will make a separate light.turn_on
|
||||
or _is_light_group(self.hass.states.get(entity_id))
|
||||
# Prevent adaptation of TURN_ON calls when light is already on,
|
||||
# and of TOGGLE calls when toggling off.
|
||||
or self.hass.states.is_state(entity_id, STATE_ON)
|
||||
or self.manual_control.get(entity_id, False)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Switch is off or light is already on for entity_id='%s', skipped='%s'"
|
||||
" (is_on='%s', is_state='%s', manual_control='%s')",
|
||||
entity_id,
|
||||
skipped,
|
||||
switch.is_on,
|
||||
self.hass.states.is_state(entity_id, STATE_ON),
|
||||
self.manual_control.get(entity_id, False),
|
||||
)
|
||||
skipped.append(entity_id)
|
||||
else:
|
||||
switch_to_eids.setdefault(switch.name, []).append(entity_id)
|
||||
switch_name_mapping[switch.name] = switch
|
||||
|
||||
entity_id = entity_ids[0]
|
||||
|
||||
# Prevent adaptation of TURN_ON calls when light is already on,
|
||||
# and of TOGGLE calls when toggling off.
|
||||
if self.hass.states.is_state(entity_id, STATE_ON):
|
||||
return
|
||||
|
||||
if self.manual_control.get(entity_id, False):
|
||||
return
|
||||
|
||||
if data.get(ATTR_BRIGHTNESS) == 0:
|
||||
# Check for `multi_light_intercept: true/false`
|
||||
mli = [sw._multi_light_intercept for sw in switch_name_mapping.values()]
|
||||
more_than_one_switch = len(switch_to_eids) > 1
|
||||
single_switch_with_multiple_lights = (
|
||||
len(switch_to_eids) == 1 and len(next(iter(switch_to_eids.values()))) > 1
|
||||
)
|
||||
switch_without_multi_light_intercept = not all(mli)
|
||||
if more_than_one_switch and switch_without_multi_light_intercept:
|
||||
_LOGGER.warning(
|
||||
"Turn-on call with zero brightness detected, Adaptive Lighting"
|
||||
" intercepted this service_call and adjusted it. If you use this as"
|
||||
" a brightness workaround, please remove it, it is no longer necessary",
|
||||
"Multiple switches (%s) targeted, but not all have"
|
||||
" `multi_light_intercept: true`, so skipping intercept"
|
||||
" for all lights.",
|
||||
switch_to_eids,
|
||||
)
|
||||
skipped = entity_ids
|
||||
switch_to_eids = {}
|
||||
elif (
|
||||
single_switch_with_multiple_lights and switch_without_multi_light_intercept
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Single switch with multiple lights targeted, but"
|
||||
" `multi_light_intercept: true` is not set, so skipping intercept"
|
||||
" for all lights.",
|
||||
switch_to_eids,
|
||||
)
|
||||
skipped = entity_ids
|
||||
switch_to_eids = {}
|
||||
|
||||
try:
|
||||
adaptive_switch = _switch_with_lights(self.hass, [entity_id])
|
||||
except NoSwitchFoundError:
|
||||
# This might be a light that is not managed by this AL instance.
|
||||
_LOGGER.debug(
|
||||
"(2) _service_interceptor_turn_on_handler: switch_to_eids='%s', skipped='%s'",
|
||||
switch_to_eids,
|
||||
skipped,
|
||||
)
|
||||
|
||||
def modify_service_data(service_data, entity_ids):
|
||||
"""Modify the service data to contain the entity IDs."""
|
||||
service_data.pop(ATTR_ENTITY_ID, None)
|
||||
service_data.pop(ATTR_AREA_ID, None)
|
||||
service_data[ATTR_ENTITY_ID] = entity_ids
|
||||
return service_data
|
||||
|
||||
# Intercept the call for first switch and call _adapt_light for the rest
|
||||
has_intercepted = False # Can only intercept a turn_on call once
|
||||
for adaptive_switch_name, _entity_ids in switch_to_eids.items():
|
||||
switch = switch_name_mapping[adaptive_switch_name]
|
||||
transition = data[CONF_PARAMS].get(
|
||||
ATTR_TRANSITION,
|
||||
switch.initial_transition,
|
||||
)
|
||||
if not has_intercepted:
|
||||
_LOGGER.debug(
|
||||
"(3) _service_interceptor_turn_on_handler: intercepting entity_ids='%s'",
|
||||
_entity_ids,
|
||||
)
|
||||
await self._service_interceptor_turn_on_single_light_handler(
|
||||
entity_ids=_entity_ids,
|
||||
switch=switch,
|
||||
transition=transition,
|
||||
call=call,
|
||||
data=modify_service_data(data, _entity_ids),
|
||||
)
|
||||
has_intercepted = True
|
||||
continue
|
||||
|
||||
for eid in _entity_ids:
|
||||
# Must add a new context otherwise _adapt_light will bail out
|
||||
context = switch.create_context("intercept")
|
||||
self.clear_proactively_adapting(eid)
|
||||
self.set_proactively_adapting(context.id, eid)
|
||||
_LOGGER.debug(
|
||||
"(4) _service_interceptor_turn_on_handler: calling `_adapt_light` with eid='%s', context='%s', transition='%s'",
|
||||
eid,
|
||||
context,
|
||||
transition,
|
||||
)
|
||||
await switch._adapt_light(
|
||||
light=eid,
|
||||
context=context,
|
||||
transition=transition,
|
||||
)
|
||||
|
||||
# Call light.turn_on service for skipped entities
|
||||
if skipped:
|
||||
if not has_intercepted:
|
||||
assert set(skipped) == set(entity_ids)
|
||||
return # The call will be intercepted with the original data
|
||||
# Call light turn_on service for skipped entities
|
||||
context = switch.create_context("skipped")
|
||||
_LOGGER.debug(
|
||||
"No (or multiple) adaptive switch(es) found for entity %s,"
|
||||
" skipping adaptation by intercepting service call",
|
||||
entity_id,
|
||||
"(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'",
|
||||
skipped,
|
||||
data,
|
||||
context.id,
|
||||
)
|
||||
# Need to expand light groups here because otherwise this interceptor loop will happen twice more
|
||||
_LOGGER.debug(
|
||||
"(6) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'",
|
||||
skipped,
|
||||
data,
|
||||
context.id,
|
||||
)
|
||||
service_data = {ATTR_ENTITY_ID: skipped, **data[CONF_PARAMS]}
|
||||
if (
|
||||
ATTR_COLOR_TEMP in service_data
|
||||
and ATTR_COLOR_TEMP_KELVIN in service_data
|
||||
):
|
||||
# ATTR_COLOR_TEMP and ATTR_COLOR_TEMP_KELVIN are mutually exclusive
|
||||
del service_data[ATTR_COLOR_TEMP]
|
||||
await self.hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
service_data,
|
||||
blocking=True,
|
||||
context=context,
|
||||
)
|
||||
return
|
||||
|
||||
if not adaptive_switch.is_on:
|
||||
return
|
||||
|
||||
if entity_id not in adaptive_switch.lights:
|
||||
return
|
||||
|
||||
async def _service_interceptor_turn_on_single_light_handler(
|
||||
self,
|
||||
entity_ids: list[str],
|
||||
switch: AdaptiveSwitch,
|
||||
transition: int,
|
||||
call: ServiceCall,
|
||||
data: ServiceData,
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Intercepted TURN_ON call with data %s (%s)",
|
||||
data,
|
||||
|
|
@ -2024,18 +2212,13 @@ class AdaptiveLightingManager:
|
|||
)
|
||||
|
||||
# Reset because turning on the light, this also happens in
|
||||
# `turn_on_off_event_listener`, however, this function is called
|
||||
# `state_changed_event_listener`, however, this function is called
|
||||
# before that one.
|
||||
self.reset(entity_id, reset_manual_control=False)
|
||||
self.reset(*entity_ids, reset_manual_control=False)
|
||||
for entity_id in entity_ids:
|
||||
self.clear_proactively_adapting(entity_id)
|
||||
|
||||
self.clear_proactively_adapting(entity_id)
|
||||
|
||||
transition = data[CONF_PARAMS].get(
|
||||
ATTR_TRANSITION,
|
||||
adaptive_switch.initial_transition,
|
||||
)
|
||||
|
||||
adaptation_data = await adaptive_switch.prepare_adaptation_data(
|
||||
adaptation_data = await switch.prepare_adaptation_data(
|
||||
entity_id,
|
||||
transition,
|
||||
)
|
||||
|
|
@ -2063,12 +2246,21 @@ class AdaptiveLightingManager:
|
|||
# We cannot know here whether there is another call to follow (since the
|
||||
# state can change until the next call), so we just schedule it and let
|
||||
# it sort out by itself.
|
||||
self.set_proactively_adapting(call.context.id, entity_id)
|
||||
self.set_proactively_adapting(adaptation_data.context.id, entity_id)
|
||||
for entity_id in entity_ids:
|
||||
self.set_proactively_adapting(call.context.id, entity_id)
|
||||
self.set_proactively_adapting(adaptation_data.context.id, entity_id)
|
||||
adaptation_data.initial_sleep = True
|
||||
_ = asyncio.create_task( # Don't await to avoid blocking the service call
|
||||
adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data),
|
||||
|
||||
# Don't await to avoid blocking the service call.
|
||||
# Assign to a variable only to await in tests.
|
||||
self.adaptation_tasks.add(
|
||||
asyncio.create_task(
|
||||
switch.execute_cancellable_adaptation_calls(adaptation_data),
|
||||
),
|
||||
)
|
||||
# Remove tasks that are done
|
||||
if done_tasks := [t for t in self.adaptation_tasks if t.done()]:
|
||||
self.adaptation_tasks.difference_update(done_tasks)
|
||||
|
||||
def _handle_timer(
|
||||
self,
|
||||
|
|
@ -2092,14 +2284,22 @@ class AdaptiveLightingManager:
|
|||
|
||||
def start_transition_timer(self, light: str) -> None:
|
||||
"""Mark a light as manually controlled."""
|
||||
last_service_data = self.last_service_data[light]
|
||||
last_transition = last_service_data.get(ATTR_TRANSITION)
|
||||
if not last_transition:
|
||||
last_service_data = self.last_service_data.get(light)
|
||||
if last_service_data is None:
|
||||
_LOGGER.debug(
|
||||
"No transition in last adapt for light %s, continuing...",
|
||||
"No last service data for light %s, not starting timer.",
|
||||
light,
|
||||
)
|
||||
return
|
||||
|
||||
last_transition = last_service_data.get(ATTR_TRANSITION)
|
||||
if not last_transition:
|
||||
_LOGGER.debug(
|
||||
"No transition in last adapt for light %s, not starting timer.",
|
||||
light,
|
||||
)
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"Start transition timer of %s seconds for light %s",
|
||||
last_transition,
|
||||
|
|
@ -2198,8 +2398,7 @@ class AdaptiveLightingManager:
|
|||
for light in lights:
|
||||
if reset_manual_control:
|
||||
self.manual_control[light] = False
|
||||
timer = self.auto_reset_manual_control_timers.pop(light, None)
|
||||
if timer is not None:
|
||||
if timer := self.auto_reset_manual_control_timers.pop(light, None):
|
||||
timer.cancel()
|
||||
self.our_last_state_on_change.pop(light, None)
|
||||
self.last_service_data.pop(light, None)
|
||||
|
|
@ -2380,6 +2579,7 @@ class AdaptiveLightingManager:
|
|||
entity_id,
|
||||
event.context.id,
|
||||
)
|
||||
# Note: the reset below already happened in `_service_interceptor_turn_on_handler`
|
||||
return
|
||||
|
||||
self.reset(entity_id, reset_manual_control=False)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@
|
|||
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
|
||||
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
|
||||
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
|
||||
"skip_redundant_commands": "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."
|
||||
"skip_redundant_commands": "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.",
|
||||
"multi_light_intercept": "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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue