Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add adapt_only_on_bare_turn_on which instantly triggers manual_control when turning on with brightness or color #709

Merged
merged 1 commit into from
Aug 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 39 additions & 39 deletions README.md

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion custom_components/adaptive_lighting/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@
"(`false`). 🔄"
)

CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON = (
"adapt_only_on_bare_turn_on",
False,
)
DOCS[CONF_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. 🕵️ "
)

CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False
DOCS[CONF_PREFER_RGB_COLOR] = (
"Whether to prefer RGB color adjustment over "
Expand Down Expand Up @@ -329,7 +341,6 @@ def int_between(min_int, max_int):
),
(CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK, int),
(CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT, int),
(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool),
(CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool),
(
Expand All @@ -338,6 +349,7 @@ def int_between(min_int, max_int):
int_between(0, 365 * 24 * 60 * 60), # 1 year max
),
(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON, bool),
(CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool),
(CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)),
(CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float),
Expand Down
3 changes: 2 additions & 1 deletion custom_components/adaptive_lighting/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@
"brightness_mode": "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": "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": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"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.",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
"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. 🕵️ ",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
Expand Down
64 changes: 56 additions & 8 deletions custom_components/adaptive_lighting/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
ATTR_ADAPT_COLOR,
ATTR_ADAPTIVE_LIGHTING_MANAGER,
CONF_ADAPT_DELAY,
CONF_ADAPT_ONLY_ON_BARE_TURN_ON,
CONF_ADAPT_UNTIL_SLEEP,
CONF_AUTORESET_CONTROL,
CONF_BRIGHTNESS_MODE,
Expand Down Expand Up @@ -876,15 +877,18 @@ def _set_changeable_settings(
self._adapt_delay = data[CONF_ADAPT_DELAY]
self._send_split_delay = data[CONF_SEND_SPLIT_DELAY]
self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
if not data[CONF_TAKE_OVER_CONTROL] and data[CONF_DETECT_NON_HA_CHANGES]:
if not data[CONF_TAKE_OVER_CONTROL] and (
data[CONF_DETECT_NON_HA_CHANGES] or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON]
):
_LOGGER.warning(
"%s: Config mismatch: 'detect_non_ha_changes: true' "
"requires 'take_over_control' to be enabled. Adjusting config "
"%s: Config mismatch: `detect_non_ha_changes` or `adapt_only_on_bare_turn_on` "
"set to `true` requires `take_over_control` to be enabled. Adjusting config "
"and continuing setup with `take_over_control: true`.",
self._name,
)
self._take_over_control = True
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]
self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS]
self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT]
Expand Down Expand Up @@ -1446,13 +1450,14 @@ async def _update_attrs_and_maybe_adapt_lights(

async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None:
assert not self.manager.is_proactively_adapting(event.context.id)
from_turn_on = self.manager._off_to_on_state_event_is_from_turn_on(
entity_id,
event,
)
if (
self._take_over_control
and not self._detect_non_ha_changes
and not self.manager._off_to_on_state_event_is_from_turn_on(
entity_id,
event,
)
and not from_turn_on
):
# There is an edge case where 2 switches control the same light, e.g.,
# one for brightness and one for color. Now we will mark both switches
Expand All @@ -1468,6 +1473,25 @@ async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> Non
self.manager.mark_as_manual_control(entity_id)
return

if (
self._take_over_control
and self._adapt_only_on_bare_turn_on
and from_turn_on
):
service_data = self.manager.turn_on_event[entity_id].data[ATTR_SERVICE_DATA]
if self.manager._mark_manual_control_if_non_bare_turn_on(
entity_id,
service_data,
):
_LOGGER.debug(
"Skipping responding to 'off' → 'on' event for '%s' with context.id='%s' because"
" we only adapt on bare `light.turn_on` events and not on service_data: '%s'",
entity_id,
event.context.id,
service_data,
)
return

if self._adapt_delay > 0:
await asyncio.sleep(self._adapt_delay)

Expand Down Expand Up @@ -2070,6 +2094,14 @@ async def _service_interceptor_turn_on_handler( # noqa: PLR0912, PLR0915
# 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)
or (
switch._take_over_control
and switch._adapt_only_on_bare_turn_on
and self._mark_manual_control_if_non_bare_turn_on(
entity_id,
data[CONF_PARAMS],
)
)
):
_LOGGER.debug(
"Switch is off or light is already on for entity_id='%s', skipped='%s'"
Expand Down Expand Up @@ -2622,6 +2654,7 @@ def is_manually_controlled(
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
):
Expand Down Expand Up @@ -2855,6 +2888,21 @@ async def just_turned_off( # noqa: PLR0911, PLR0912
)
return False

def _mark_manual_control_if_non_bare_turn_on(
self,
entity_id: str,
service_data: ServiceData,
) -> bool:
_LOGGER.debug(
"_mark_manual_control_if_non_bare_turn_on: entity_id='%s', service_data='%s'",
entity_id,
service_data,
)
if any(attr in service_data for attr in COLOR_ATTRS | BRIGHTNESS_ATTRS):
self.mark_as_manual_control(entity_id)
return True
return False


class _AsyncSingleShotTimer:
def __init__(self, delay, callback) -> None:
Expand Down
3 changes: 2 additions & 1 deletion custom_components/adaptive_lighting/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@
"brightness_mode": "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": "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": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"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.",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
"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. 🕵️ ",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
Expand Down
22 changes: 19 additions & 3 deletions tests/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
SERVICE_CHANGE_SWITCH_SETTINGS,
SERVICE_SET_MANUAL_CONTROL,
SLEEP_MODE_SWITCH,
CONF_ADAPT_ONLY_ON_BARE_TURN_ON,
UNDO_UPDATE_LISTENER,
)
from custom_components.adaptive_lighting.switch import (
Expand Down Expand Up @@ -561,9 +562,19 @@ async def test_manager_not_tracking_untracked_lights(hass):
assert light not in switch.manager.lights


async def test_manual_control(hass):
@pytest.mark.parametrize("adapt_only_on_bare_turn_on", [True, False])
@pytest.mark.parametrize("proactive_service_call_adaptation", [True, False])
async def test_manual_control(
hass, adapt_only_on_bare_turn_on, proactive_service_call_adaptation
):
"""Test the 'manual control' tracking."""
switch, (light, *_) = await setup_lights_and_switch(hass)
switch, (light, *_) = await setup_lights_and_switch(
hass,
{
CONF_ADAPT_ONLY_ON_BARE_TURN_ON: adapt_only_on_bare_turn_on,
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation,
},
)
assert switch._take_over_control
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON

Expand Down Expand Up @@ -639,9 +650,14 @@ def increased_color_temp():
await change_manual_control(True)
assert manual_control[ENTITY_LIGHT_1]
await turn_light(False)
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
assert not manual_control[ENTITY_LIGHT_1], manual_control
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

# Check that toggling (sleep mode) switch resets manual control
for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]:
Expand Down