mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-22 18:04:19 +02:00
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:
parent
11bd5cf2aa
commit
9e29a21197
11 changed files with 235 additions and 7 deletions
|
|
@ -12,8 +12,10 @@ except ImportError:
|
|||
from homeassistant.components.adaptive_lighting.const import (
|
||||
BASIC_OPTIONS,
|
||||
CONF_INITIAL_TRANSITION,
|
||||
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON,
|
||||
CONF_SUNRISE_TIME,
|
||||
CONF_SUNSET_TIME,
|
||||
DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
NONE_STR,
|
||||
|
|
@ -149,6 +151,10 @@ async def test_options_schema_has_each_setting_once(hass):
|
|||
advanced = _advanced_section(result)
|
||||
|
||||
assert advanced.options == {"collapsed": True}
|
||||
assert (
|
||||
_schema_defaults(advanced.schema)[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON]
|
||||
is DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON
|
||||
)
|
||||
assert {key.schema for key in schema if key.schema != "advanced"} == BASIC_OPTIONS
|
||||
assert {key.schema for key in advanced.schema.schema} == set(
|
||||
DEFAULT_DATA,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ from homeassistant.components.adaptive_lighting.const import (
|
|||
CONF_DETECT_NON_HA_CHANGES,
|
||||
CONF_INITIAL_TRANSITION,
|
||||
CONF_MANUAL_CONTROL,
|
||||
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON,
|
||||
CONF_MAX_BRIGHTNESS,
|
||||
CONF_MAX_COLOR_TEMP,
|
||||
CONF_MIN_BRIGHTNESS,
|
||||
|
|
@ -4473,6 +4474,152 @@ async def test_automation_turn_on_from_off_not_marked_as_manual_control(hass):
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("intercept", [True, False])
|
||||
async def test_manual_control_on_external_turn_on_allows_tracked_service_call(
|
||||
hass,
|
||||
intercept,
|
||||
):
|
||||
"""Test a real HA turn-on remains eligible for initial adaptation."""
|
||||
switch, _ = await setup_lights_and_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON: True,
|
||||
CONF_DETECT_NON_HA_CHANGES: True,
|
||||
CONF_INTERCEPT: intercept,
|
||||
CONF_MIN_BRIGHTNESS: 50,
|
||||
CONF_MAX_BRIGHTNESS: 50,
|
||||
},
|
||||
)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: ENTITY_LIGHT_1},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 200},
|
||||
blocking=True,
|
||||
context=Context(id=f"ha_turn_on_{intercept}"),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_LIGHT_1)
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == 128
|
||||
assert (
|
||||
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.NONE
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("intercept", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"manual_control_on_external_turn_on",
|
||||
"detect_non_ha_changes",
|
||||
"expected_manual_control",
|
||||
"expected_adaptation",
|
||||
),
|
||||
[
|
||||
(True, True, LightControlAttributes.ALL, False),
|
||||
(True, False, LightControlAttributes.ALL, False),
|
||||
(False, True, LightControlAttributes.NONE, True),
|
||||
(False, False, LightControlAttributes.ALL, False),
|
||||
],
|
||||
)
|
||||
async def test_manual_control_on_external_turn_on_external_state_change(
|
||||
hass,
|
||||
freezer,
|
||||
intercept,
|
||||
manual_control_on_external_turn_on,
|
||||
detect_non_ha_changes,
|
||||
expected_manual_control,
|
||||
expected_adaptation,
|
||||
):
|
||||
"""Test an unmatched off-to-on state event follows the opt-in policy."""
|
||||
switch, _ = await setup_lights_and_switch(
|
||||
hass,
|
||||
{
|
||||
"manual_control_on_external_turn_on": manual_control_on_external_turn_on,
|
||||
CONF_DETECT_NON_HA_CHANGES: detect_non_ha_changes,
|
||||
CONF_INTERCEPT: intercept,
|
||||
CONF_MIN_BRIGHTNESS: 50,
|
||||
CONF_MAX_BRIGHTNESS: 50,
|
||||
},
|
||||
)
|
||||
external_attributes = dict(hass.states.get(ENTITY_LIGHT_1).attributes)
|
||||
external_attributes[ATTR_BRIGHTNESS] = 200
|
||||
hass.states.async_set(
|
||||
ENTITY_LIGHT_1,
|
||||
STATE_OFF,
|
||||
external_attributes,
|
||||
context=Context(id=f"unmatched_turn_off_{intercept}"),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF
|
||||
freezer.tick(6)
|
||||
|
||||
hass.states.async_set(
|
||||
ENTITY_LIGHT_1,
|
||||
STATE_ON,
|
||||
external_attributes,
|
||||
context=Context(id=f"unmatched_turn_on_{intercept}"),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
== expected_manual_control
|
||||
)
|
||||
last_service_data = switch.manager.last_service_data.get(ENTITY_LIGHT_1)
|
||||
if expected_adaptation:
|
||||
assert last_service_data[ATTR_BRIGHTNESS] == 128
|
||||
else:
|
||||
assert last_service_data is None
|
||||
assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize("intercept", [True, False])
|
||||
async def test_manual_control_on_external_turn_on_keeps_non_ha_change_detection(
|
||||
hass,
|
||||
intercept,
|
||||
):
|
||||
"""Test the option does not disable manual tracking for an on light."""
|
||||
switch, (light, *_) = await setup_lights_and_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON: True,
|
||||
CONF_DETECT_NON_HA_CHANGES: True,
|
||||
CONF_INTERCEPT: intercept,
|
||||
CONF_MIN_BRIGHTNESS: 50,
|
||||
CONF_MAX_BRIGHTNESS: 50,
|
||||
},
|
||||
)
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
context=switch.create_context("test"),
|
||||
transition=0,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128
|
||||
|
||||
set_light_brightness(light, 200)
|
||||
light.async_write_ha_state()
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
context=switch.create_context("test"),
|
||||
transition=0,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 200
|
||||
assert (
|
||||
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
|
||||
== LightControlAttributes.BRIGHTNESS
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("intercept", [True, False])
|
||||
async def test_adapt_only_on_bare_turn_on_respects_pause_changed_mode(hass, intercept):
|
||||
"""Test that adapt_only_on_bare_turn_on respects take_over_control_mode=PAUSE_CHANGED.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue