refactor: use manual control methods for state access

This commit is contained in:
Mario Guggenberger 2026-01-13 16:54:27 +00:00
commit 4c7786bbc6
No known key found for this signature in database
GPG key ID: 4BF213E6EF2D7EA1
2 changed files with 102 additions and 62 deletions

View file

@ -1127,7 +1127,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
extra_state_attributes[key] = None
return extra_state_attributes
extra_state_attributes["manual_control"] = [
light for light in self.lights if self.manager.manual_control.get(light)
light
for light in self.lights
if self.manager.get_manual_control_attributes(light).has_any()
]
extra_state_attributes.update(self._settings)
timers = self.manager.auto_reset_manual_control_timers
@ -1713,7 +1715,7 @@ class AdaptiveLightingManager:
# Locks that prevent light adjusting when waiting for a light to 'turn_off'
self.turn_off_locks: dict[str, asyncio.Lock] = {}
# Tracks which lights are manually controlled
self.manual_control: dict[str, LightControlAttributes] = {}
self._manual_control: dict[str, LightControlAttributes] = {}
# Track 'state_changed' events of self.lights resulting from this integration
self.our_last_state_on_change: dict[str, list[State]] = {}
# Track last 'service_data' to 'light.turn_on' resulting from this integration
@ -1878,7 +1880,7 @@ class AdaptiveLightingManager:
skipped,
switch.is_on,
self.hass.states.is_state(entity_id, STATE_ON),
self.manual_control.get(entity_id, False),
self.get_manual_control_attributes(entity_id),
switch._intercept,
)
skipped.append(entity_id)
@ -2215,7 +2217,7 @@ class AdaptiveLightingManager:
light: str,
) -> LightControlAttributes:
"""Get the attributes for a light that are manually controlled."""
return self.manual_control.get(light, LightControlAttributes.NONE)
return self._manual_control.get(light, LightControlAttributes.NONE)
def set_manual_control_attributes(
self,
@ -2229,7 +2231,7 @@ class AdaptiveLightingManager:
attributes,
self.get_manual_control_attributes(light),
)
self.manual_control[light] = attributes
self._manual_control[light] = attributes
delay = self.auto_reset_manual_control_times.get(light)
async def reset() -> None:
@ -2250,7 +2252,7 @@ class AdaptiveLightingManager:
transition=switch.initial_transition,
force=True,
)
assert self.manual_control[light] == LightControlAttributes.NONE
assert self.get_manual_control_attributes(light).has_none()
self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset)
@ -2347,7 +2349,7 @@ class AdaptiveLightingManager:
"Light %s: Clearing manual control attributes.",
light,
)
self.manual_control[light] = LightControlAttributes.NONE
self.set_manual_control_attributes(light, LightControlAttributes.NONE)
if timer := self.auto_reset_manual_control_timers.pop(light, None):
timer.cancel()
self.our_last_state_on_change.pop(light, None)

View file

@ -632,7 +632,6 @@ async def test_manual_control(
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
context = switch.create_context("test") # needs to be passed to update method
manual_control = switch.manager.manual_control
async def update():
await switch._update_attrs_and_maybe_adapt_lights(context=context, transition=0)
@ -661,7 +660,7 @@ async def test_manual_control(
async def change_manual_control(set_to, extra_service_data=None):
if extra_service_data is None:
extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT_1]}
_LOGGER.debug(f"{switch.manager.manual_control=}")
_LOGGER.debug(f"{switch.manager._manual_control=}")
await hass.services.async_call(
DOMAIN,
SERVICE_SET_MANUAL_CONTROL,
@ -672,7 +671,7 @@ async def test_manual_control(
},
blocking=True,
)
_LOGGER.debug(f"{switch.manager.manual_control=}")
_LOGGER.debug(f"{switch.manager._manual_control=}")
_LOGGER.debug("Called set_manual_control with %s", set_to)
await hass.async_block_till_done()
await update()
@ -689,21 +688,24 @@ async def test_manual_control(
# Nothing is manually controlled
await update()
assert not manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
# Call light.turn_on for ENTITY_LIGHT_1
await turn_light(True, brightness=increased_brightness())
# Check that ENTITY_LIGHT_1 is manually controlled
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.BRIGHTNESS
)
# Test adaptive_lighting.set_manual_control
await change_manual_control(False)
# Check that ENTITY_LIGHT_1 is not manually controlled
assert not manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
# Check that toggling light off to on resets manual control
await change_manual_control(True)
assert manual_control[ENTITY_LIGHT_1]
assert switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
await turn_light(False)
assert not manual_control[ENTITY_LIGHT_1], manual_control
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
await turn_light(True, brightness=increased_brightness())
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
# Turning on from OFF with brightness:
@ -711,44 +713,52 @@ async def test_manual_control(
# - With adapt_only_on_bare_turn_on=False: should NOT mark (fix for issue #1378)
if adapt_only_on_bare_turn_on:
assert (
manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS
), manual_control
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.BRIGHTNESS
)
else:
assert not manual_control[ENTITY_LIGHT_1], manual_control
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
# Reset for next test
await turn_light(False)
await turn_light(True)
assert not manual_control[ENTITY_LIGHT_1], manual_control
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
# Now change brightness while ON - this should always be manual control
await turn_light(True, brightness=increased_brightness())
assert (
manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS
), manual_control
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.BRIGHTNESS
)
# Check that toggling (sleep mode) switch resets manual control
for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]:
await change_manual_control(True)
assert manual_control[ENTITY_LIGHT_1]
assert switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
await turn_switch(False, entity_id)
await turn_switch(True, entity_id)
assert not manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
# Check that manual control is still enabled if set while bulb is off.
# Test issue #37
await turn_light(False)
await change_manual_control(True)
await turn_light(True)
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.ALL
)
# Check that when 'adapt_brightness' is off, changing the brightness
# doesn't mark it as manually controlled but changing color_temp
# does
await turn_light(False)
await turn_light(True) # reset manually controlled status
assert not manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
await switch.adapt_brightness_switch.async_turn_off()
await turn_light(True, brightness=increased_brightness())
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.BRIGHTNESS
)
mired_range = (light.min_color_temp_kelvin, light.max_color_temp_kelvin)
kelvin_range = (
color_temperature_mired_to_kelvin(mired_range[1]),
@ -759,7 +769,10 @@ async def test_manual_control(
True,
color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin,
)
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.ALL
)
await switch.adapt_brightness_switch.async_turn_on() # turn on again
# Check that when 'adapt_color' is off, changing the color
@ -767,12 +780,18 @@ async def test_manual_control(
# does
await turn_light(False) # reset manually controlled status
await turn_light(True)
assert not manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
await switch.adapt_color_switch.async_turn_off()
await turn_light(True, color_temp_kelvin=increased_color_temp())
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.COLOR
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.COLOR
)
await turn_light(True, brightness=increased_brightness())
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.ALL
)
# Check that when 'adapt_color' adapt_brightness are both off
# nothing marks it as manually controlled
@ -780,7 +799,7 @@ async def test_manual_control(
await turn_light(True)
await switch.adapt_color_switch.async_turn_off()
await switch.adapt_brightness_switch.async_turn_off()
assert not manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
await turn_light(True, color_temp_kelvin=increased_color_temp())
await turn_light(True, brightness=increased_brightness())
await turn_light(
@ -788,17 +807,26 @@ async def test_manual_control(
color_temp_kelvin=increased_color_temp(),
brightness=increased_brightness(),
)
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.ALL
)
# Turn switches on again
await switch.adapt_color_switch.async_turn_on()
await switch.adapt_brightness_switch.async_turn_on()
# Check that when no lights are specified, all are reset
await change_manual_control(True, {CONF_LIGHTS: switch.lights})
assert all(manual_control[eid] for eid in switch.lights)
assert all(
switch.manager.get_manual_control_attributes(eid).has_all()
for eid in switch.lights
)
# do not pass "lights" so reset all
await change_manual_control(False, {})
assert all(not manual_control[eid] for eid in switch.lights)
assert all(
switch.manager.get_manual_control_attributes(eid).has_none()
for eid in switch.lights
)
# Turn off light and turn on using adaptive_lighting.apply
await turn_light(False)
@ -814,23 +842,32 @@ async def test_manual_control(
)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
assert not manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
# Check that manual control `True` sets all attributes
await change_manual_control(False)
assert not manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
await change_manual_control(True)
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.ALL
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.ALL
)
# Check that manual control `False` unsets all attributes
await change_manual_control(False)
assert not manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
# Check that manual control attributes can be selectively set
await change_manual_control("brightness")
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.BRIGHTNESS
)
await change_manual_control("color")
assert manual_control[ENTITY_LIGHT_1] == LightControlAttributes.COLOR
assert (
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.COLOR
)
@flaky(max_runs=3, min_passes=1)
@ -840,7 +877,6 @@ async def test_auto_reset_manual_control(hass):
{CONF_AUTORESET_CONTROL: 0.1},
)
context = switch.create_context("test") # needs to be passed to update method
manual_control = switch.manager.manual_control
async def update():
await switch._update_attrs_and_maybe_adapt_lights(context=context, transition=0)
@ -865,13 +901,16 @@ async def test_auto_reset_manual_control(hass):
_LOGGER.debug("Start test auto reset manual control")
await turn_light(True, brightness=1)
await turn_light(True, brightness=10)
assert manual_control[light.entity_id] == LightControlAttributes.BRIGHTNESS
assert (
switch.manager.get_manual_control_attributes(light.entity_id)
== LightControlAttributes.BRIGHTNESS
)
assert (
switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0
)
await update()
await asyncio.sleep(0.3) # Should be enough time for auto reset
assert not manual_control[light.entity_id], (light, manual_control)
assert not switch.manager.get_manual_control_attributes(light.entity_id)
assert (
light.entity_id not in switch.extra_state_attributes["autoreset_time_remaining"]
)
@ -881,11 +920,11 @@ async def test_auto_reset_manual_control(hass):
_LOGGER.debug("Quick change %s", i)
await turn_light(True, brightness=(i + 1) * 20)
await asyncio.sleep(0.05) # Less than 0.1
assert manual_control[light.entity_id]
assert switch.manager.get_manual_control_attributes(light.entity_id)
await update()
await asyncio.sleep(0.3) # Wait the auto reset time
assert not manual_control[light.entity_id]
assert not switch.manager.get_manual_control_attributes(light.entity_id)
async def test_adaptation_attribute_selection(hass):
@ -1116,7 +1155,7 @@ async def test_switch_off_on_off(hass):
# Turn light off with transition
await turn_light(False, transition=1)
assert not switch.manager.manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
# Set state to on after a second (like happens IRL)
await asyncio.sleep(1e-3)
hass.states.async_set(ENTITY_LIGHT_1, STATE_ON)
@ -1371,9 +1410,9 @@ async def test_state_change_handlers(hass):
assert timer.is_running()
last_service_data = deepcopy(current_service_data)
await update()
assert not switch.manager.manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
await update()
assert not switch.manager.manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
timer = listener.transition_timers.get(ENTITY_LIGHT_1)
assert timer
assert timer.is_running()
@ -1397,19 +1436,19 @@ async def test_state_change_handlers(hass):
await turn_light(True, brightness=20)
await update(force=False)
assert (
switch.manager.manual_control[ENTITY_LIGHT_1]
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.BRIGHTNESS
)
await update(force=True)
assert (
switch.manager.manual_control[ENTITY_LIGHT_1]
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.BRIGHTNESS
)
# turn light off then on should reset manual control.
await turn_light(False)
await turn_light(True)
assert not switch.manager.manual_control[ENTITY_LIGHT_1]
assert not switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
await turn_light(True, brightness=50)
_LOGGER.debug("Test: Brightness set to %s", 50)
@ -1419,7 +1458,7 @@ async def test_state_change_handlers(hass):
assert switch.manager.last_service_data.get(ENTITY_LIGHT_1) is not None
assert switch.manager.our_last_state_on_change.get(ENTITY_LIGHT_1) is not None
assert (
switch.manager.manual_control[ENTITY_LIGHT_1]
switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
== LightControlAttributes.BRIGHTNESS
)
@ -2288,12 +2327,11 @@ async def test_light_group(
await hass.async_block_till_done()
if take_over_control:
assert switch.manager.manual_control["light.light_4"]
assert switch.manager.manual_control["light.light_5"]
assert switch.manager.get_manual_control_attributes("light.light_4")
assert switch.manager.get_manual_control_attributes("light.light_5")
else:
assert not switch.manager.manual_control["light.light_4"]
assert not switch.manager.manual_control["light.light_5"]
assert not switch.manager.get_manual_control_attributes("light.light_4")
assert not switch.manager.get_manual_control_attributes("light.light_5")
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
@ -2302,8 +2340,8 @@ async def test_light_group(
)
await hass.async_block_till_done()
assert not switch.manager.manual_control["light.light_4"]
assert not switch.manager.manual_control["light.light_5"]
assert not switch.manager.get_manual_control_attributes("light.light_4")
assert not switch.manager.get_manual_control_attributes("light.light_5")
events = await _turn_on_and_track_event_contexts(
hass,
"testing",
@ -2796,7 +2834,7 @@ async def test_automation_turn_on_from_off_not_marked_as_manual_control(hass):
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF
# Verify light is not manually controlled
assert not switch.manager.manual_control.get(
assert not switch.manager.get_manual_control_attributes(
ENTITY_LIGHT_1,
), "Light should not be manually controlled before test"
@ -2821,7 +2859,7 @@ async def test_automation_turn_on_from_off_not_marked_as_manual_control(hass):
# CRITICAL: The light should NOT be marked as manually controlled!
# The bug in v1.30.0 would incorrectly mark this as manual control because
# the turn_on had a brightness attribute.
manual_control_attrs = switch.manager.manual_control.get(ENTITY_LIGHT_1)
manual_control_attrs = switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
assert not manual_control_attrs, (
f"Bug confirmed: Light was incorrectly marked as manually controlled "
f"(attributes: {manual_control_attrs}) when turned on from OFF state. "
@ -2895,7 +2933,7 @@ async def test_adapt_only_on_bare_turn_on_respects_pause_changed_mode(hass, inte
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
# 1. Verify that ONLY brightness is marked as manually controlled
manual_control_attrs = switch.manager.manual_control.get(ENTITY_LIGHT_1)
manual_control_attrs = switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1)
assert manual_control_attrs == LightControlAttributes.BRIGHTNESS, (
f"Expected only BRIGHTNESS to be marked as manually controlled, "
f"but got: {manual_control_attrs}. With adapt_only_on_bare_turn_on=True, "