mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-12 06:44:04 +02:00
fix: preserve Home Assistant area target exclusions (#1511)
* Exclude 'service' lights (entity_category) from area intercept turn-on Fixes #1510 When Adaptive Lighting intercepts an area/label-targeted light.turn_on, the intercept rewrites the call to target only the managed lights (see modify_service_data), so Home Assistant's own handler turns on only those. AL then re-issues light.turn_on for the remaining 'skipped' (unmanaged) entities so they still come on. The problem: HA excludes entities with an entity_category (config/diagnostic, e.g. the Home Assistant Voice LED ring) from area/label expansion, but AL's re-issue did not, so AL was the sole thing turning these service lights on. Changes: - Keep re-issuing skipped lights so unmanaged normal lights still come on, but filter out 'service' lights (entity_category set) from that re-issue. - Add _is_service_light helper (registry-based) and divert a *managed* service light to 'skipped' in _separate_entity_ids, so it is excluded from the intercept turn-on while still being adapted when on. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add toggle regression test for service light exclusion from area intercept Mirrors test_service_light_excluded_from_area_intercept_turn_on but uses light.toggle: a service light (entity_category set) in an area must remain off when AL intercepts an area toggle. The managed lights still toggle on. Refs #1510, PR #1511. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Only exclude service lights from indirect (area/device/label) expansion Previously _is_service_light filtered service lights unconditionally, which also excluded a service light explicitly named in `entity_id`. Home Assistant only excludes such lights from indirect area/device/label expansion and turns them on when directly targeted, so AL must mirror that: service lights are now excluded from the intercept/re-issue only when not directly targeted (`direct_entity_ids`). Adds a regression test for the direct-target case. Refs #1510, PR #1511. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use if/elif/else for indirect service-light exclusion; inline check Address review: collapse the two separate `if is_service`/"if not is_service" into an `if/elif/else` chain (ruff PLR5501) and inline the service check as `self._is_service_light(...) and entity_id not in direct_entity_ids`, which conceptually is `is_indirect_service`. Matches HA's indirect-only exclusion and keeps the flow simple. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Filter service lights directly in _get_entity_list expansion Move service-light filtering to the single expansion site AdaptiveLightingManager._get_entity_list. Area/device expansion is the only place where HA excludes entity_category lights, so filtering there mirrors HA and makes the later direct_entity_ids / skipped_normal guards unnecessary. Explicit entity_id targets bypass expansion and therefore still turn on service lights, matching HA. Fully reverts the direct_entity_ids / skipped_normal addition per review. --------- Co-authored-by: mueslo <mueslo@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bas Nijholt <basnijholt@gmail.com>
This commit is contained in:
parent
e453541a78
commit
68c0e4db69
2 changed files with 85 additions and 1 deletions
|
|
@ -1859,6 +1859,13 @@ class AdaptiveLightingManager:
|
|||
self._context_cnt += 1
|
||||
return context
|
||||
|
||||
def _is_excluded_from_area(self, entity_id: str) -> bool:
|
||||
"""Match Home Assistant's exclusions for indirect area targets."""
|
||||
entry = entity_registry.async_get(self.hass).async_get(entity_id)
|
||||
return entry is not None and (
|
||||
entry.entity_category is not None or entry.hidden_by is not None
|
||||
)
|
||||
|
||||
def _separate_entity_ids(
|
||||
self,
|
||||
entity_ids: list[str],
|
||||
|
|
@ -2398,6 +2405,7 @@ class AdaptiveLightingManager:
|
|||
entity_id
|
||||
for entity_id in area_entity_ids
|
||||
if entity_id.startswith(LIGHT_DOMAIN)
|
||||
and not self._is_excluded_from_area(entity_id)
|
||||
]
|
||||
entity_ids.extend(eids)
|
||||
_LOGGER.debug(
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ from homeassistant.components.adaptive_lighting.const import (
|
|||
CONF_INITIAL_TRANSITION,
|
||||
CONF_MANUAL_CONTROL,
|
||||
CONF_MAX_BRIGHTNESS,
|
||||
CONF_MIN_BRIGHTNESS,
|
||||
CONF_MIN_COLOR_TEMP,
|
||||
CONF_MULTI_LIGHT_INTERCEPT,
|
||||
CONF_PREFER_RGB_COLOR,
|
||||
|
|
@ -112,6 +113,7 @@ from homeassistant.const import (
|
|||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.const import __version__ as ha_version
|
||||
from homeassistant.core import Context, Event, HomeAssistant, State
|
||||
|
|
@ -1299,7 +1301,11 @@ async def test_state_change_handlers(hass):
|
|||
4. Assert all possible problems that would result.
|
||||
Also tests significant changes.
|
||||
"""
|
||||
switch, (light, *_) = await setup_lights_and_switch(hass)
|
||||
# Keep adaptive brightness distinct from the manual values 20, 40, and 50.
|
||||
switch, (light, *_) = await setup_lights_and_switch(
|
||||
hass,
|
||||
{CONF_MIN_BRIGHTNESS: 50, CONF_MAX_BRIGHTNESS: 50},
|
||||
)
|
||||
context = switch.create_context("test") # needs to be passed to update method
|
||||
|
||||
# [Config options]:
|
||||
|
|
@ -3339,3 +3345,73 @@ def test_validate_yaml_data_wins_over_stray_options():
|
|||
result = validate(entry)
|
||||
|
||||
assert result[CONF_LIGHTS] == ["light.a"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TOGGLE])
|
||||
@pytest.mark.parametrize("explicit", [False, True], ids=["area", "direct"])
|
||||
@pytest.mark.parametrize("managed", [False, True], ids=["unmanaged", "managed"])
|
||||
@pytest.mark.parametrize(
|
||||
"registry_settings",
|
||||
[
|
||||
{},
|
||||
{"entity_category": EntityCategory.CONFIG},
|
||||
{"entity_category": EntityCategory.DIAGNOSTIC},
|
||||
{"hidden_by": entity_registry.RegistryEntryHider.USER},
|
||||
],
|
||||
ids=["normal", "config", "diagnostic", "hidden"],
|
||||
)
|
||||
async def test_intercept_preserves_area_target_exclusions(
|
||||
hass: HomeAssistant,
|
||||
service: str,
|
||||
explicit: bool,
|
||||
managed: bool,
|
||||
registry_settings: dict[str, Any],
|
||||
):
|
||||
"""Area calls exclude hidden/categorized lights; direct calls honor them."""
|
||||
await setup_lights(hass)
|
||||
mock_area_registry(hass)
|
||||
registry = entity_registry.async_get(hass)
|
||||
lights = [ENTITY_LIGHT_1, ENTITY_LIGHT_2, ENTITY_LIGHT_3]
|
||||
for light in lights:
|
||||
registry.async_update_entity(light, area_id="test-area")
|
||||
registry.async_update_entity(ENTITY_LIGHT_3, **registry_settings)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: lights},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await setup_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_LIGHTS: (
|
||||
[ENTITY_LIGHT_1, ENTITY_LIGHT_3] if managed else [ENTITY_LIGHT_1]
|
||||
),
|
||||
CONF_INTERCEPT: True,
|
||||
CONF_INITIAL_TRANSITION: 0,
|
||||
CONF_TRANSITION: 0,
|
||||
CONF_MIN_BRIGHTNESS: 50,
|
||||
CONF_MAX_BRIGHTNESS: 50,
|
||||
},
|
||||
)
|
||||
assert all(hass.states.get(light).state == STATE_OFF for light in lights)
|
||||
|
||||
target = {ATTR_ENTITY_ID: lights} if explicit else {ATTR_AREA_ID: "test-area"}
|
||||
await hass.services.async_call(LIGHT_DOMAIN, service, target, blocking=True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Both normal lights turn on; only the managed one gets adaptive brightness.
|
||||
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
|
||||
assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128
|
||||
assert hass.states.get(ENTITY_LIGHT_2).state == STATE_ON
|
||||
assert hass.states.get(ENTITY_LIGHT_2).attributes.get(ATTR_BRIGHTNESS) != 128
|
||||
target_state = hass.states.get(ENTITY_LIGHT_3)
|
||||
if registry_settings and not explicit:
|
||||
assert target_state.state == STATE_OFF
|
||||
else:
|
||||
assert target_state.state == STATE_ON
|
||||
if managed:
|
||||
assert target_state.attributes[ATTR_BRIGHTNESS] == 128
|
||||
else:
|
||||
assert target_state.attributes.get(ATTR_BRIGHTNESS) != 128
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue