mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-12 14:54:04 +02:00
Make instantaneous light.turn_on adaptation configurable with intercept (#750)
* Make intercept configurable * Update README.md, strings.json, and services.yaml * import * skip * split * do not pass config_entry * add to conf * Update README.md, strings.json, and services.yaml * spacing --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
f540057093
commit
68feb3d931
6 changed files with 65 additions and 57 deletions
|
|
@ -138,7 +138,8 @@ The YAML and frontend configuration methods support all of the options listed be
|
|||
| `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` |
|
||||
| `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. | `True` | `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` |
|
||||
|
||||
<!-- END_OUTPUT -->
|
||||
|
|
|
|||
|
|
@ -231,6 +231,13 @@ DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = (
|
|||
"Disable if physical light states get out of sync with HA's recorded state."
|
||||
)
|
||||
|
||||
CONF_INTERCEPT, DEFAULT_INTERCEPT = "intercept", True
|
||||
DOCS[CONF_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."
|
||||
)
|
||||
|
||||
CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = (
|
||||
"multi_light_intercept",
|
||||
True,
|
||||
|
|
@ -238,7 +245,8 @@ CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = (
|
|||
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."
|
||||
"into multiple calls, e.g., when lights are in different switches. "
|
||||
"Requires `intercept` to be enabled."
|
||||
)
|
||||
|
||||
SLEEP_MODE_SWITCH = "sleep_mode_switch"
|
||||
|
|
@ -356,6 +364,7 @@ VALIDATION_TUPLES = [
|
|||
DEFAULT_SKIP_REDUNDANT_COMMANDS,
|
||||
bool,
|
||||
),
|
||||
(CONF_INTERCEPT, DEFAULT_INTERCEPT, bool),
|
||||
(CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool),
|
||||
(CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@
|
|||
"send_split_delay": "send_split_delay",
|
||||
"adapt_delay": "adapt_delay",
|
||||
"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.",
|
||||
"intercept": "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.",
|
||||
"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. Requires `intercept` to be enabled.",
|
||||
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝"
|
||||
},
|
||||
"data_description": {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ from .const import (
|
|||
CONF_DETECT_NON_HA_CHANGES,
|
||||
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES,
|
||||
CONF_INITIAL_TRANSITION,
|
||||
CONF_INTERCEPT,
|
||||
CONF_INTERVAL,
|
||||
CONF_LIGHTS,
|
||||
CONF_MANUAL_CONTROL,
|
||||
|
|
@ -180,11 +181,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
# A (non-user-configurable, thus internal) flag to control the proactive adaptation mode.
|
||||
# This exists to disable the proactive adaptation in the unit tests and enable it
|
||||
# only for specific unit tests and when running as integration."""
|
||||
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION = "proactive_adaptation"
|
||||
|
||||
# Consider it a significant change when attribute changes more than
|
||||
BRIGHTNESS_CHANGE = 25 # ≈10% of total range
|
||||
COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500)
|
||||
|
|
@ -427,7 +423,7 @@ async def async_setup_entry( # noqa: PLR0915
|
|||
return
|
||||
|
||||
if (manager := data.get(ATTR_ADAPTIVE_LIGHTING_MANAGER)) is None:
|
||||
manager = AdaptiveLightingManager(hass, config_entry)
|
||||
manager = AdaptiveLightingManager(hass)
|
||||
data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = manager
|
||||
|
||||
sleep_mode_switch = SimpleSwitch(
|
||||
|
|
@ -882,7 +878,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
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]
|
||||
self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS]
|
||||
self._intercept = data[CONF_INTERCEPT]
|
||||
self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT]
|
||||
if not data[CONF_INTERCEPT] and data[CONF_MULTI_LIGHT_INTERCEPT]:
|
||||
_LOGGER.warning(
|
||||
"%s: Config mismatch: `multi_light_intercept` set to `true` requires `intercept`"
|
||||
" to be enabled. Adjusting config and continuing setup with"
|
||||
" `multi_light_intercept: false`.",
|
||||
self._name,
|
||||
)
|
||||
self._multi_light_intercept = False
|
||||
self._expand_light_groups() # updates manual control timers
|
||||
location, _ = get_astral_location(self.hass)
|
||||
|
||||
|
|
@ -1603,11 +1608,10 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
|
|||
class AdaptiveLightingManager:
|
||||
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the AdaptiveLightingManager that is shared among all switches."""
|
||||
assert hass is not None
|
||||
self.hass = hass
|
||||
data = validate(config_entry)
|
||||
self.lights: set[str] = set()
|
||||
|
||||
# Tracks 'light.turn_off' service calls
|
||||
|
|
@ -1658,38 +1662,32 @@ class AdaptiveLightingManager:
|
|||
|
||||
self._proactively_adapting_contexts: dict[str, str] = {}
|
||||
|
||||
is_proactive_adaptation_enabled = data.get(
|
||||
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION,
|
||||
True,
|
||||
)
|
||||
try:
|
||||
self.listener_removers.append(
|
||||
setup_service_call_interceptor(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
self._service_interceptor_turn_on_handler,
|
||||
),
|
||||
)
|
||||
|
||||
if is_proactive_adaptation_enabled:
|
||||
try:
|
||||
self.listener_removers.append(
|
||||
setup_service_call_interceptor(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
self._service_interceptor_turn_on_handler,
|
||||
),
|
||||
)
|
||||
self.listener_removers.append(
|
||||
setup_service_call_interceptor(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TOGGLE,
|
||||
self._service_interceptor_turn_on_handler,
|
||||
),
|
||||
)
|
||||
|
||||
self.listener_removers.append(
|
||||
setup_service_call_interceptor(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TOGGLE,
|
||||
self._service_interceptor_turn_on_handler,
|
||||
),
|
||||
)
|
||||
|
||||
_LOGGER.debug("Proactive adaptation enabled")
|
||||
except RuntimeError:
|
||||
_LOGGER.warning(
|
||||
"Failed to set up service call interceptors, "
|
||||
"falling back to event-reactive mode",
|
||||
exc_info=True,
|
||||
)
|
||||
_LOGGER.debug("Proactive adaptation enabled")
|
||||
except RuntimeError:
|
||||
_LOGGER.warning(
|
||||
"Failed to set up service call interceptors, "
|
||||
"falling back to event-reactive mode",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
def disable(self):
|
||||
"""Disable the listener by removing all subscribed handlers."""
|
||||
|
|
@ -1812,6 +1810,7 @@ class AdaptiveLightingManager:
|
|||
else:
|
||||
if (
|
||||
not switch.is_on
|
||||
or not switch._intercept
|
||||
# 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,
|
||||
|
|
@ -1829,12 +1828,13 @@ class AdaptiveLightingManager:
|
|||
):
|
||||
_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')",
|
||||
" (is_on='%s', is_state='%s', manual_control='%s', switch._intercept='%s')",
|
||||
entity_id,
|
||||
skipped,
|
||||
switch.is_on,
|
||||
self.hass.states.is_state(entity_id, STATE_ON),
|
||||
self.manual_control.get(entity_id, False),
|
||||
switch._intercept,
|
||||
)
|
||||
skipped.append(entity_id)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@
|
|||
"send_split_delay": "send_split_delay",
|
||||
"adapt_delay": "adapt_delay",
|
||||
"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.",
|
||||
"intercept": "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.",
|
||||
"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. Requires `intercept` to be enabled.",
|
||||
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝"
|
||||
},
|
||||
"data_description": {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ from custom_components.adaptive_lighting.const import (
|
|||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
from custom_components.adaptive_lighting.switch import (
|
||||
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION,
|
||||
CONF_INTERCEPT,
|
||||
AdaptiveSwitch,
|
||||
_attributes_have_changed,
|
||||
color_difference_redmean,
|
||||
|
|
@ -167,7 +167,7 @@ async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitc
|
|||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: False,
|
||||
CONF_INTERCEPT: False,
|
||||
**extra_data,
|
||||
},
|
||||
)
|
||||
|
|
@ -579,7 +579,7 @@ async def test_manual_control(
|
|||
hass,
|
||||
{
|
||||
CONF_ADAPT_ONLY_ON_BARE_TURN_ON: adapt_only_on_bare_turn_on,
|
||||
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation,
|
||||
CONF_INTERCEPT: proactive_service_call_adaptation,
|
||||
},
|
||||
)
|
||||
assert switch._take_over_control
|
||||
|
|
@ -1470,9 +1470,7 @@ def _mock_sun_light_settings(switch: AdaptiveSwitch, settings: dict[str, Any]):
|
|||
|
||||
async def test_proactive_adaptation(hass):
|
||||
"""Validate that a proactive adaptation updates the original service call."""
|
||||
switch, _ = await setup_lights_and_switch(
|
||||
hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True
|
||||
)
|
||||
switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True)
|
||||
|
||||
_mock_sun_light_settings(
|
||||
switch,
|
||||
|
|
@ -1503,7 +1501,7 @@ async def test_proactive_adaptation_with_separate_commands(hass):
|
|||
switch, _ = await setup_lights_and_switch(
|
||||
hass,
|
||||
{
|
||||
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True,
|
||||
CONF_INTERCEPT: True,
|
||||
CONF_SEPARATE_TURN_ON_COMMANDS: True,
|
||||
},
|
||||
True,
|
||||
|
|
@ -1539,9 +1537,7 @@ async def test_proactive_adaptation_toggle(hass):
|
|||
|
||||
This test is based on the fact that contexts of proactive adaptations are recorded.
|
||||
"""
|
||||
switch, _ = await setup_lights_and_switch(
|
||||
hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True
|
||||
)
|
||||
switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True)
|
||||
|
||||
# Toggle ON
|
||||
await hass.services.async_call(
|
||||
|
|
@ -1571,7 +1567,7 @@ async def test_proactive_adaptation_transition_override(hass):
|
|||
switch, (_, _, light3) = await setup_lights_and_switch(
|
||||
hass,
|
||||
{
|
||||
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True,
|
||||
CONF_INTERCEPT: True,
|
||||
CONF_INITIAL_TRANSITION: 123,
|
||||
},
|
||||
True,
|
||||
|
|
@ -1628,7 +1624,7 @@ async def setup_proactive_multiple_lights_two_switches(hass):
|
|||
CONF_DETECT_NON_HA_CHANGES: True,
|
||||
CONF_PREFER_RGB_COLOR: False,
|
||||
CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp}
|
||||
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True,
|
||||
CONF_INTERCEPT: True,
|
||||
}
|
||||
_, switch1 = await setup_switch(
|
||||
hass, {CONF_NAME: "switch1", CONF_LIGHTS: [ENTITY_LIGHT_1], **defaults}
|
||||
|
|
@ -1753,7 +1749,7 @@ async def test_two_switches_for_single_light(hass):
|
|||
|
||||
One switch for brightness and another for color.
|
||||
"""
|
||||
extra_conf = {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}
|
||||
extra_conf = {CONF_INTERCEPT: True}
|
||||
switch1, (light1, *_) = await setup_lights_and_switch(
|
||||
hass, extra_conf | {CONF_NAME: "switch1"}, all_lights=True
|
||||
)
|
||||
|
|
@ -1928,7 +1924,7 @@ async def test_light_group(
|
|||
hass,
|
||||
{
|
||||
CONF_LIGHTS: entity_ids,
|
||||
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation,
|
||||
CONF_INTERCEPT: proactive_service_call_adaptation,
|
||||
CONF_TAKE_OVER_CONTROL: take_over_control,
|
||||
CONF_MULTI_LIGHT_INTERCEPT: multi_light_intercept,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue