From 8f78233e11d83b70eadde2cf95a48c12dfcdc2e3 Mon Sep 17 00:00:00 2001 From: Marijn Eken Date: Sun, 6 Sep 2026 10:43:37 +0200 Subject: [PATCH 1/2] docs: clarify that UI setup needs no YAML entry (#1031) * Fixed outdated info in README.md The README says to always add an adaptive_lighting: entry in the YAML, where this seems to be no longer needed (or even preferred). * docs: clarify YAML is optional for UI setup --------- Co-authored-by: Bas Nijholt Co-authored-by: Bas Nijholt --- README.md | 2 +- docs/configuration.md | 10 ++-------- docs/getting-started.md | 41 +++++++++++++---------------------------- docs/index.md | 15 ++------------- 4 files changed, 18 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index d3e3ed2c..72a77484 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ adaptive_lighting: lights: - light.living_room_lights ``` -Note: If you plan to strictly use the UI, the `adaptive_lighting:` entry must still be added to the YAML. +If you configure Adaptive Lighting through the UI, no `adaptive_lighting:` entry is needed in `configuration.yaml`. Instances configured through YAML must be edited in YAML. Transform your home's atmosphere with Adaptive Lighting 🏠, and experience the benefits of intelligent, sun-synchronized lighting today! diff --git a/docs/configuration.md b/docs/configuration.md index 21c7028e..237a53ae 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -8,17 +8,11 @@ Adaptive Lighting supports configuration through both YAML and the Home Assistan ## Basic Configuration -The minimal configuration requires only adding the integration to your `configuration.yaml`: - -```yaml -adaptive_lighting: -``` - -You can then configure everything through the UI at **Settings** → **Devices & Services** → **Adaptive Lighting** → **Configure**. +The simplest setup uses the Home Assistant UI. Go to **Settings** → **Devices & Services** → **Add Integration** → **Adaptive Lighting**. No `adaptive_lighting:` entry is needed in `configuration.yaml`. ## YAML Configuration -For YAML configuration, you can specify lights and options directly: +Alternatively, you can specify lights and options in `configuration.yaml`: ```yaml adaptive_lighting: diff --git a/docs/getting-started.md b/docs/getting-started.md index 68c253eb..48826e84 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -34,40 +34,23 @@ Or use this button to open HACS directly: ## Configuration -### Step 1: Add to configuration.yaml - -Add the following to your `configuration.yaml`: - -```yaml -adaptive_lighting: -``` - -> [!NOTE] -> This entry is required even if you plan to configure everything through the UI. - -### Step 2: Restart Home Assistant - -Restart Home Assistant for the changes to take effect. - -### Step 3: Add the Integration - -1. Go to **Settings** → **Devices & Services** -2. Click **+ Add Integration** -3. Search for "Adaptive Lighting" -4. Follow the setup wizard to select your lights - -### Step 4: Configure Your Lights - -You can configure Adaptive Lighting in two ways: +Choose one of two configuration methods: === "Via UI" 1. Go to **Settings** → **Devices & Services** - 2. Find Adaptive Lighting and click **Configure** - 3. Adjust settings as needed + 2. Click **+ Add Integration** + 3. Search for "Adaptive Lighting" + 4. Follow the setup wizard to name your Adaptive Lighting instance + 5. Find Adaptive Lighting and click **Configure** + 6. Select your lights and adjust the settings + + No `adaptive_lighting:` entry is needed in `configuration.yaml`. === "Via YAML" + Instances configured through YAML must be edited in YAML. + ```yaml adaptive_lighting: - name: "Living Room" @@ -80,7 +63,9 @@ You can configure Adaptive Lighting in two ways: max_color_temp: 5500 ``` -## Basic Configuration Example + Restart Home Assistant after changing the YAML configuration. + +## Basic YAML Configuration Example Here's a simple configuration to get you started: diff --git a/docs/index.md b/docs/index.md index bc3acebe..f01cbfa2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -46,19 +46,8 @@ Adaptive Lighting provides four switches (using "living_room" as an example comp ## Quick Start 1. **Install via HACS**: Search for "Adaptive Lighting" in the [Home Assistant Community Store](https://hacs.xyz/) -2. **Add to configuration**: Add `adaptive_lighting:` to your `configuration.yaml` -3. **Configure**: Go to **Settings** → **Devices & Services** → **Add Integration** → **Adaptive Lighting** -4. **Select your lights**: Choose which lights to control and enjoy automatic adaptation! - -```yaml -# Minimal configuration.yaml entry -adaptive_lighting: - lights: - - light.living_room -``` - -> [!TIP] -> **Using the UI exclusively?** Even if you plan to configure everything through the UI, the `adaptive_lighting:` entry must still be present in your `configuration.yaml`. +2. **Add the integration**: Go to **Settings** → **Devices & Services** → **Add Integration** → **Adaptive Lighting**, then name your instance +3. **Configure**: Open Adaptive Lighting, click **Configure**, select your lights, and adjust the settings. No YAML entry is needed. [Get Started →](getting-started.md){ .md-button .md-button--primary } [View All Options →](configuration.md){ .md-button } From fdce4c9102126e5486201a921f4370d4b820cc64 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 10:45:08 +0200 Subject: [PATCH 2/2] fix: keep adaptation updates from restarting manual-control timers (#1525) * Fix autoreset timeout during partial adaptation * Test manual timeout renewal for mixed light requests --- custom_components/adaptive_lighting/switch.py | 4 +- tests/test_switch.py | 140 +++++++++++++++++- 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 44ad9c60..eb62d7b6 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2497,9 +2497,11 @@ class AdaptiveLightingManager: if ( timer is not None and timer.is_running() + and not is_our_context(event.context) + and not self.is_proactively_adapting(event.context.id) and event.time_fired > timer.start_time # type: ignore[operator] ): - # Restart the auto reset timer + # Only external turn-ons extend manual control, not our adaptations. timer.start() if service == SERVICE_TURN_OFF: diff --git a/tests/test_switch.py b/tests/test_switch.py index b96e56fe..52b1dd27 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -895,10 +895,11 @@ async def test_manual_control( @flaky(max_runs=3, min_passes=1) -async def test_auto_reset_manual_control(hass): +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +async def test_auto_reset_manual_control(hass, mode): switch, (light, *_) = await setup_lights_and_switch( hass, - {CONF_AUTORESET_CONTROL: 0.1}, + {CONF_AUTORESET_CONTROL: 0.1, CONF_TAKE_OVER_CONTROL_MODE: mode}, ) context = switch.create_context("test") # needs to be passed to update method manual_control = switch.manager.manual_control @@ -965,6 +966,141 @@ async def test_auto_reset_manual_control(hass): assert not manual_control[light.entity_id] +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + ("attribute", "value", "next_value", "manual_attributes"), + [ + (ATTR_BRIGHTNESS, 10, 20, LightControlAttributes.BRIGHTNESS), + (ATTR_COLOR_TEMP_KELVIN, 2000, 2200, LightControlAttributes.COLOR), + ], +) +async def test_interval_adaptation_preserves_manual_control_timeout( + hass, + freezer, + cleanup, + intercept, + mode, + attribute, + value, + next_value, + manual_attributes, +): + """Adaptation must not postpone auto reset; another manual change must.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: intercept, + }, + ) + + async def change_manually(value): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, attribute: value}, + blocking=True, + ) + await hass.async_block_till_done() + + for manual_value in (value, next_value): + # A new external change to the already-manual axis restarts the timer. + await change_manually(manual_value) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + for elapsed in (90, 180): + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attributes + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][ + light.entity_id + ] + == 7200 - elapsed + ) + + # An external bare turn-on also keeps its existing timer restart behavior. + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize("service_data", [{}, {ATTR_BRIGHTNESS: 20}]) +async def test_mixed_turn_on_restarts_manual_control_timeout( + hass, + freezer, + cleanup, + mode, + service_data, +): + """A mixed-target request must renew manual control on its skipped light.""" + switch, (manual_light, _, off_light) = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: True, + }, + all_lights=True, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: manual_light.entity_id, ATTR_BRIGHTNESS: 10}, + blocking=True, + ) + await hass.async_block_till_done() + freezer.tick(90) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: [manual_light.entity_id, off_light.entity_id], + **service_data, + }, + blocking=True, + ) + await hass.async_block_till_done() + + # HA dispatches the original external event before interception. It renews + # manual control even though the skipped target is replayed in our context. + assert hass.states.is_state(off_light.entity_id, STATE_ON) + assert is_our_context( + switch.manager.turn_on_event[manual_light.entity_id].context, + "skipped", + ) + assert ( + switch.manager.get_manual_control_attributes(manual_light.entity_id) + == LightControlAttributes.BRIGHTNESS + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][ + manual_light.entity_id + ] + == 7200 + ) + + async def test_adaptation_attribute_selection(hass): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch(hass)