Add manual_control_on_external_turn_on option (#1490)

* feat: add `adapt_only_on_ha_turn_on` to skip adapting externally turned-on lights

When a light turns on from `off` via a source outside Home Assistant — a
physical wall switch or a hub/manufacturer scene (e.g. Lutron) — and
`detect_non_ha_changes` is enabled, Adaptive Lighting adapts the light on the
resulting `off` → `on` event, overriding the brightness/color the external
source just set. Disabling `detect_non_ha_changes` avoids this but also stops
detection of manual changes to already-on lights; the two behaviors were
coupled to a single flag.

Add `adapt_only_on_ha_turn_on` (default `false`, requires `take_over_control`).
When enabled, an `off` → `on` transition with no matching HA `light.turn_on`
context is marked `manual_control` and left untouched, independent of
`detect_non_ha_changes`, decoupling the two behaviors.

The off→on guard reduces to the previous expression when the option is `false`,
so existing configurations are unaffected. Includes a parametrized regression
test, docs, and regenerated strings/services/README via
scripts/update-generated-content.

Refs #435

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Shorten generated turn-on option description

* Document shared turn-on policy limitations

* Name external turn-on policy after manual-control behavior

* Clarify settings needed to adapt unmatched turn-ons

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Bas Nijholt <bas@nijho.lt>
This commit is contained in:
Alistair Galbraith 2026-09-06 12:02:30 -07:00 committed by GitHub
commit 9e29a21197
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 235 additions and 7 deletions

View file

@ -100,6 +100,16 @@ DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = (
"Needs `take_over_control` enabled. 🕵️"
)
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON = (
"manual_control_on_external_turn_on",
False,
)
DOCS[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] = (
"Treat turn-ons without a matching Home Assistant `light.turn_on` context as "
"manual control. Normal manual-control resets apply. Still allows "
"`detect_non_ha_changes` for already-on lights. 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 "
@ -416,6 +426,11 @@ VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [
),
(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON, bool),
(
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON,
DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON,
bool,
),
(
CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE,
DEFAULT_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE,

View file

@ -238,6 +238,12 @@ change_switch_settings:
example: false
selector:
boolean: null
manual_control_on_external_turn_on:
description: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️
required: false
example: false
selector:
boolean: null
transition:
description: Duration of transition when lights change, in seconds. 🕑
required: false

View file

@ -70,6 +70,7 @@
"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 and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️",
"manual_control_on_external_turn_on": "manual_control_on_external_turn_on: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️",
"reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴",
"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",
@ -270,6 +271,10 @@
"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"
},
"manual_control_on_external_turn_on": {
"description": "Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️",
"name": "manual_control_on_external_turn_on"
},
"transition": {
"description": "Duration of transition when lights change, in seconds. 🕑",
"name": "transition"

View file

@ -104,6 +104,7 @@ from .const import (
CONF_INTERVAL,
CONF_LIGHTS,
CONF_MANUAL_CONTROL,
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON,
CONF_MAX_BRIGHTNESS,
CONF_MAX_COLOR_TEMP,
CONF_MAX_SUNRISE_TIME,
@ -955,12 +956,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._send_split_delay = data[CONF_SEND_SPLIT_DELAY]
self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
if not data[CONF_TAKE_OVER_CONTROL] and (
data[CONF_DETECT_NON_HA_CHANGES] or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON]
data[CONF_DETECT_NON_HA_CHANGES]
or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON]
or data[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON]
):
_LOGGER.warning(
"%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`.",
"%s: Config mismatch: `detect_non_ha_changes`, `adapt_only_on_bare_turn_on`, "
"or `manual_control_on_external_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
@ -969,6 +972,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
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._manual_control_on_external_turn_on = data[
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON
]
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
self._reset_manual_control_on_sleep_mode_change = data[
CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE
@ -1603,16 +1609,26 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
if (
self._take_over_control
and not self._detect_non_ha_changes
and (
not self._detect_non_ha_changes
or self._manual_control_on_external_turn_on
)
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
# as manually controlled, which is not 100% correct.
#
# This 'off' → 'on' event does not exactly match the most recently tracked
# `light.turn_on` context for the entity. Hand control over when either:
# - `detect_non_ha_changes` is False (we can't reliably track manual changes
# to already-on lights anyway), or
# - `manual_control_on_external_turn_on` is True (the user explicitly wants external
# turn-ons left untouched, even while `detect_non_ha_changes` is enabled).
_LOGGER.debug(
"%s: Ignoring 'off''on' event for '%s' with context.id='%s'"
" because 'light.turn_on' was not called by HA and"
" 'detect_non_ha_changes' is False",
" because it does not match a tracked 'light.turn_on' context and"
" ('detect_non_ha_changes' is False or 'manual_control_on_external_turn_on' is True)",
self._name,
entity_id,
event.context.id,

View file

@ -71,6 +71,7 @@
"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 and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️",
"manual_control_on_external_turn_on": "manual_control_on_external_turn_on: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️",
"reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴",
"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",
@ -271,6 +272,10 @@
"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"
},
"manual_control_on_external_turn_on": {
"description": "Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️",
"name": "manual_control_on_external_turn_on"
},
"transition": {
"description": "Duration of transition when lights change, in seconds. 🕑",
"name": "transition"