From 68e243c87e69c97030d222846c0c0771bccc94ae Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Nov 2025 09:30:01 -0800 Subject: [PATCH] Fix infinite loop when disabling SimpleSwitch entities (#1296) * Add regression tests for SimpleSwitch initial state bug Adds tests that verify SimpleSwitch._state is set immediately in __init__ rather than waiting for async_added_to_hass(). These tests currently FAIL because _state is None after __init__, which causes an infinite loop in _setup_listeners when the entity is disabled (since async_added_to_hass is never called for disabled entities). Regression tests for: https://github.com/basnijholt/adaptive-lighting/issues/1264 * Fix infinite loop when disabling SimpleSwitch entities The issue was that SimpleSwitch._state was initialized to None in __init__, but only set to a boolean value in async_added_to_hass(). When an entity is disabled, async_added_to_hass() is never called, so _state stayed None. The _setup_listeners() method has a while loop that waits for _state is not None for all SimpleSwitch children (sleep_mode_switch, adapt_brightness_switch, adapt_color_switch). With _state stuck at None, this created an infinite loop. The fix sets _state to initial_state directly in __init__ instead of waiting for async_added_to_hass() to set it. The async_added_to_hass() will still properly restore state from the last session or set based on initial_state as before. Fixes: https://github.com/basnijholt/adaptive-lighting/issues/1264 --- custom_components/adaptive_lighting/switch.py | 2 +- tests/test_switch.py | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 6a22a41f..19e99d78 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1563,7 +1563,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity): self.hass = hass data = validate(config_entry) self._icon = icon - self._state: bool | None = None + self._state: bool = initial_state self._which = which self._config_name = data[CONF_NAME] self._unique_id = f"{self._config_name}_{slugify(self._which)}" diff --git a/tests/test_switch.py b/tests/test_switch.py index fc5f67cc..1016b591 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -65,6 +65,7 @@ from homeassistant.components.adaptive_lighting.switch import ( CONF_INTERCEPT, AdaptiveLightingManager, AdaptiveSwitch, + SimpleSwitch, _attributes_have_changed, color_difference_redmean, create_context, @@ -2275,3 +2276,76 @@ async def test_brightness_mode(hass, brightness_mode, dark, light): # After sunrise the brightness should be light_brightness await patch_time_and_update(after_sunrise) assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness) + + +async def test_simple_switch_initial_state_not_none(hass): + """Test that SimpleSwitch._state is not None after __init__. + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1264 + + When an entity is disabled in Home Assistant, async_added_to_hass() is never + called. Previously, SimpleSwitch._state was initialized to None and only set + to True/False in async_added_to_hass(). This caused an infinite loop in + AdaptiveSwitch._setup_listeners() which waits for all SimpleSwitch._state + to be not None. + + The fix is to initialize _state to the initial_state value in __init__. + """ + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME}) + entry.add_to_hass(hass) + + # Create a SimpleSwitch without calling async_added_to_hass + # (simulating a disabled entity) + switch = SimpleSwitch( + which="Test", + initial_state=True, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + + # Before the fix: _state would be None, causing infinite loop + # After the fix: _state should be the initial_state value + assert switch._state is not None, ( + "SimpleSwitch._state should not be None after __init__. " + "This would cause an infinite loop in _setup_listeners when the entity is disabled." + ) + assert switch._state is True # Should be the initial_state value + + +async def test_simple_switch_state_after_async_added_to_hass(hass): + """Test that SimpleSwitch._state is properly set after async_added_to_hass. + + This ensures the fix for #1264 doesn't break normal entity initialization. + """ + entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME}) + entry.add_to_hass(hass) + + # Create switches with different initial states + switch_true = SimpleSwitch( + which="Test True", + initial_state=True, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + switch_false = SimpleSwitch( + which="Test False", + initial_state=False, + hass=hass, + config_entry=entry, + icon="mdi:test", + ) + + # Verify initial state is set correctly + assert switch_true._state is True + assert switch_false._state is False + + # Call async_added_to_hass (simulating normal entity setup) + # Since there's no last state, it should use the initial_state + await switch_true.async_added_to_hass() + await switch_false.async_added_to_hass() + + # State should still be correct after async_added_to_hass + assert switch_true._state is True + assert switch_false._state is False