From 249d18689e7e528c59f58478be8c6be4112c46a6 Mon Sep 17 00:00:00 2001 From: Casey Date: Sat, 16 May 2026 16:23:23 +0200 Subject: [PATCH] Fix OptionsFlowWithReload incompatibility with add_update_listener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production HA caught this on the first save attempt: HA rejects `config_entry.add_update_listener()` when the integration also uses `OptionsFlowWithReload`, raising: ValueError: Config entry update listeners should not be used with OptionsFlowWithReload The listener was a vestigial bit of the pre-redesign reload plumbing that should have been deleted alongside the manual reload path (design decision D6). Removed: - `config_entry.add_update_listener(async_update_options)` registration in `async_setup_entry` - `async_update_options` helper (only existed to call `async_reload`) - `UNDO_UPDATE_LISTENER` tracking in `async_unload_entry` - The `UNDO_UPDATE_LISTENER` import (still defined in const.py for any external code that imports it; pruning that is a separate cleanup) Updated `tests/test_init.py::test_successful_setup_on_current_version` to assert `entry.entry_id in hass.data[DOMAIN]` instead of the listener key. All 94 tests still pass locally. This is the first real-HA-only bug we've shipped — props to the production deploy for catching it before merge. --- .ha_snapshot_pre_cdit.json | 315 ++++++++++++++++++ .../adaptive_lighting/__init__.py | 19 +- tests/test_init.py | 5 +- 3 files changed, 326 insertions(+), 13 deletions(-) create mode 100644 .ha_snapshot_pre_cdit.json diff --git a/.ha_snapshot_pre_cdit.json b/.ha_snapshot_pre_cdit.json new file mode 100644 index 00000000..9906e2d9 --- /dev/null +++ b/.ha_snapshot_pre_cdit.json @@ -0,0 +1,315 @@ +{ + "master_switches": [ + "switch.adaptive_lighting_brightness_ent", + "switch.adaptive_lighting_color_ent", + "switch.adaptive_lighting_dining_mvp", + "switch.adaptive_lighting_ent", + "switch.adaptive_lighting_ko_circadian_lights", + "switch.adaptive_lighting_lr_dark_lights", + "switch.adaptive_lighting_no_brightness_control", + "switch.adaptive_lighting_other_circadian_lights", + "switch.adaptive_lighting_pre_release", + "switch.adaptive_lighting_sleep_ent" + ], + "configurations": { + "switch.adaptive_lighting_brightness_ent": {}, + "switch.adaptive_lighting_color_ent": {}, + "switch.adaptive_lighting_dining_mvp": { + "lights": [ + "light.liv_lt_lights", + "light.kit_lt_tablelamp" + ], + "interval": 90.0, + "transition": 45.0, + "initial_transition": 1.0, + "min_brightness": 49, + "max_brightness": 82, + "min_color_temp": 2000, + "max_color_temp": 4000, + "prefer_rgb_color": false, + "sleep_brightness": 1, + "sleep_rgb_or_color_temp": "color_temp", + "sleep_color_temp": 1000, + "sleep_rgb_color": [ + 255, + 56, + 0 + ], + "sleep_transition": 1.0, + "transition_until_sleep": false, + "sunrise_time": null, + "min_sunrise_time": null, + "max_sunrise_time": null, + "sunrise_offset": 0.0, + "sunset_time": null, + "min_sunset_time": null, + "max_sunset_time": null, + "sunset_offset": 0.0, + "brightness_mode": "tanh", + "brightness_mode_time_dark": 900.0, + "brightness_mode_time_light": 3600.0, + "take_over_control": false, + "take_over_control_mode": "pause_all", + "detect_non_ha_changes": false, + "autoreset_control_seconds": 0, + "only_once": false, + "adapt_only_on_bare_turn_on": false, + "separate_turn_on_commands": false, + "send_split_delay": 0, + "adapt_delay": 0.0, + "skip_redundant_commands": true, + "intercept": true, + "multi_light_intercept": true, + "include_config_in_attributes": true, + "name": "dining_mvp" + }, + "switch.adaptive_lighting_ent": { + "lights": [ + "light.hlu_lt_1" + ], + "interval": 90.0, + "transition": 45.0, + "initial_transition": 1.0, + "min_brightness": 10, + "max_brightness": 100, + "min_color_temp": 2000, + "max_color_temp": 5500, + "prefer_rgb_color": false, + "sleep_brightness": 1, + "sleep_rgb_or_color_temp": "color_temp", + "sleep_color_temp": 1000, + "sleep_rgb_color": [ + 255, + 56, + 0 + ], + "sleep_transition": 1.0, + "transition_until_sleep": false, + "sunrise_time": null, + "min_sunrise_time": null, + "max_sunrise_time": null, + "sunrise_offset": 0.0, + "sunset_time": null, + "min_sunset_time": null, + "max_sunset_time": null, + "sunset_offset": 0.0, + "brightness_mode": "tanh", + "brightness_mode_time_dark": 900.0, + "brightness_mode_time_light": 3600.0, + "take_over_control": false, + "take_over_control_mode": "pause_all", + "detect_non_ha_changes": false, + "autoreset_control_seconds": 0, + "only_once": false, + "adapt_only_on_bare_turn_on": false, + "separate_turn_on_commands": false, + "send_split_delay": 0, + "adapt_delay": 0.0, + "skip_redundant_commands": true, + "intercept": true, + "multi_light_intercept": true, + "include_config_in_attributes": true, + "name": "entrance_lights" + }, + "switch.adaptive_lighting_ko_circadian_lights": { + "lights": [ + "light.hll_lt_main", + "light.oke_lt_terrarium", + "light.oke_lt_strip", + "light.oke_lt_ceil", + "light.oke_lt_bs" + ], + "interval": 90.0, + "transition": 15.0, + "initial_transition": 1.0, + "min_brightness": 40, + "max_brightness": 70, + "min_color_temp": 2000, + "max_color_temp": 5996, + "prefer_rgb_color": false, + "sleep_brightness": 5, + "sleep_rgb_or_color_temp": "color_temp", + "sleep_color_temp": 1000, + "sleep_rgb_color": [ + 255, + 56, + 0 + ], + "sleep_transition": 1.0, + "transition_until_sleep": false, + "sunrise_time": null, + "min_sunrise_time": null, + "max_sunrise_time": null, + "sunrise_offset": 0.0, + "sunset_time": null, + "min_sunset_time": null, + "max_sunset_time": null, + "sunset_offset": 0.0, + "brightness_mode": "tanh", + "brightness_mode_time_dark": 900.0, + "brightness_mode_time_light": 3600.0, + "take_over_control": false, + "take_over_control_mode": "pause_all", + "detect_non_ha_changes": false, + "autoreset_control_seconds": 0, + "only_once": false, + "adapt_only_on_bare_turn_on": false, + "separate_turn_on_commands": false, + "send_split_delay": 0, + "adapt_delay": 0.0, + "skip_redundant_commands": true, + "intercept": true, + "multi_light_intercept": true, + "include_config_in_attributes": true, + "name": "ko_circadian_lights" + }, + "switch.adaptive_lighting_lr_dark_lights": { + "lights": [ + "light.liv_massive_standing_li", + "light.liv_lt_ceiling" + ], + "interval": 90.0, + "transition": 45.0, + "initial_transition": 1.0, + "min_brightness": 18, + "max_brightness": 34, + "min_color_temp": 2000, + "max_color_temp": 5500, + "prefer_rgb_color": false, + "sleep_brightness": 1, + "sleep_rgb_or_color_temp": "color_temp", + "sleep_color_temp": 1000, + "sleep_rgb_color": [ + 255, + 56, + 0 + ], + "sleep_transition": 1.0, + "transition_until_sleep": false, + "sunrise_time": null, + "min_sunrise_time": null, + "max_sunrise_time": null, + "sunrise_offset": 0.0, + "sunset_time": null, + "min_sunset_time": null, + "max_sunset_time": null, + "sunset_offset": 0.0, + "brightness_mode": "default", + "brightness_mode_time_dark": 900.0, + "brightness_mode_time_light": 3600.0, + "take_over_control": false, + "take_over_control_mode": "pause_all", + "detect_non_ha_changes": false, + "autoreset_control_seconds": 0, + "only_once": false, + "adapt_only_on_bare_turn_on": false, + "separate_turn_on_commands": false, + "send_split_delay": 0, + "adapt_delay": 0.0, + "skip_redundant_commands": false, + "intercept": true, + "multi_light_intercept": true, + "include_config_in_attributes": true, + "name": "lr dark lights" + }, + "switch.adaptive_lighting_no_brightness_control": { + "lights": [ + "light.hwr_ceiling", + "light.wc_lt_1" + ], + "interval": 60.0, + "transition": 5.0, + "initial_transition": 1.0, + "min_brightness": 100, + "max_brightness": 100, + "min_color_temp": 2000, + "max_color_temp": 5500, + "prefer_rgb_color": false, + "sleep_brightness": 100, + "sleep_rgb_or_color_temp": "color_temp", + "sleep_color_temp": 1000, + "sleep_rgb_color": [ + 255, + 56, + 0 + ], + "sleep_transition": 1.0, + "transition_until_sleep": false, + "sunrise_time": null, + "min_sunrise_time": null, + "max_sunrise_time": null, + "sunrise_offset": 0.0, + "sunset_time": null, + "min_sunset_time": null, + "max_sunset_time": null, + "sunset_offset": 0.0, + "brightness_mode": "tanh", + "brightness_mode_time_dark": 900.0, + "brightness_mode_time_light": 3600.0, + "take_over_control": false, + "take_over_control_mode": "pause_all", + "detect_non_ha_changes": false, + "autoreset_control_seconds": 0, + "only_once": false, + "adapt_only_on_bare_turn_on": false, + "separate_turn_on_commands": false, + "send_split_delay": 0, + "adapt_delay": 0.0, + "skip_redundant_commands": true, + "intercept": true, + "multi_light_intercept": true, + "include_config_in_attributes": true, + "name": "no_brightness_control" + }, + "switch.adaptive_lighting_other_circadian_lights": { + "lights": [ + "light.oem_lt_closet" + ], + "interval": 90.0, + "transition": 15.0, + "initial_transition": 1.0, + "min_brightness": 10, + "max_brightness": 100, + "min_color_temp": 2200, + "max_color_temp": 4000, + "prefer_rgb_color": false, + "sleep_brightness": 1, + "sleep_rgb_or_color_temp": "color_temp", + "sleep_color_temp": 1000, + "sleep_rgb_color": [ + 255, + 56, + 0 + ], + "sleep_transition": 1.0, + "transition_until_sleep": false, + "sunrise_time": null, + "min_sunrise_time": null, + "max_sunrise_time": null, + "sunrise_offset": 0.0, + "sunset_time": null, + "min_sunset_time": null, + "max_sunset_time": null, + "sunset_offset": 0.0, + "brightness_mode": "tanh", + "brightness_mode_time_dark": 900.0, + "brightness_mode_time_light": 3600.0, + "take_over_control": false, + "take_over_control_mode": "pause_all", + "detect_non_ha_changes": false, + "autoreset_control_seconds": 0, + "only_once": false, + "adapt_only_on_bare_turn_on": false, + "separate_turn_on_commands": false, + "send_split_delay": 0, + "adapt_delay": 0.0, + "skip_redundant_commands": true, + "intercept": true, + "multi_light_intercept": true, + "include_config_in_attributes": true, + "name": "other_circadian_lights" + }, + "switch.adaptive_lighting_pre_release": {}, + "switch.adaptive_lighting_sleep_ent": {} + } +} \ No newline at end of file diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py index fed756c2..e1c05594 100644 --- a/custom_components/adaptive_lighting/__init__.py +++ b/custom_components/adaptive_lighting/__init__.py @@ -17,7 +17,6 @@ from .const import ( CONF_NAME, CONFIG_ENTRY_VERSION, DOMAIN, - UNDO_UPDATE_LISTENER, ) _LOGGER = logging.getLogger(__name__) @@ -116,7 +115,12 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Set up the component.""" + """Set up the component. + + Note: `OptionsFlowWithReload` (in config_flow.py) handles reload-on-save + automatically — HA rejects `config_entry.add_update_listener` when used + with `OptionsFlowWithReload`, so we deliberately do NOT register one. + """ _remove_orphan_sleep_entities(hass, config_entry) data = hass.data.setdefault(DOMAIN, {}) @@ -125,18 +129,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # and explicit `hass.reload_config_entry` calls). hass.bus.async_listen("hass.config.entry_updated", reload_configuration_yaml) - undo_listener = config_entry.add_update_listener(async_update_options) - data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener} + data[config_entry.entry_id] = {} await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True -async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: - """Update options.""" - await hass.config_entries.async_reload(config_entry.entry_id) - - async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_forward_entry_unload( @@ -144,9 +142,8 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> "switch", ) data = hass.data[DOMAIN] - data[config_entry.entry_id][UNDO_UPDATE_LISTENER]() if unload_ok: - data.pop(config_entry.entry_id) + data.pop(config_entry.entry_id, None) if len(data) == 1 and ATTR_ADAPTIVE_LIGHTING_MANAGER in data: # no more config_entries diff --git a/tests/test_init.py b/tests/test_init.py index 13e30687..214afe16 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -16,7 +16,6 @@ from custom_components.adaptive_lighting.const import ( CONFIG_ENTRY_VERSION, DEFAULT_NAME, DOMAIN, - UNDO_UPDATE_LISTENER, ) # --------------------------------------------------------------------------- @@ -35,7 +34,9 @@ async def test_successful_setup_on_current_version(hass) -> None: entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.LOADED - assert UNDO_UPDATE_LISTENER in hass.data[DOMAIN][entry.entry_id] + # OptionsFlowWithReload handles reload internally — we don't register + # an update listener (HA rejects the combination). + assert entry.entry_id in hass.data[DOMAIN] async def test_stale_version_raises_config_entry_error(hass, caplog) -> None: