From db254d06c9706361d1e5fbfba3f4a3bdbde19f90 Mon Sep 17 00:00:00 2001 From: Marijn Eken Date: Tue, 6 Aug 2024 14:46:55 +0200 Subject: [PATCH 1/6] 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). --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37b29dc3..5974aaac 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,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. +Note: ~~If you plan to strictly use the UI, the `adaptive_lighting:` entry must still be added to the YAML.~~ This isn't true (anymore?). If you do add an entry to the YAML, the options page will stay empty and uneditable. Removing the entry and adding the integration via de UI, enables all options. Transform your home's atmosphere with Adaptive Lighting 🏠, and experience the benefits of intelligent, sun-synchronized lighting today! From 0f606fb935ca318baefe90f6c3e21342c80c1fa4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 01:30:32 -0700 Subject: [PATCH 2/6] chore: prepare version 1.32.0b1 --- custom_components/adaptive_lighting/manifest.json | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json index 427b730a..c4369c59 100644 --- a/custom_components/adaptive_lighting/manifest.json +++ b/custom_components/adaptive_lighting/manifest.json @@ -8,5 +8,5 @@ "iot_class": "calculated", "issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues", "requirements": ["ulid-transform"], - "version": "1.31.0" + "version": "1.32.0b1" } diff --git a/pyproject.toml b/pyproject.toml index bdaf9acf..9d6c28d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "adaptive-lighting" -version = "1.30.1" +version = "1.32.0b1" description = "Automatically adjust brightness and color of lights based on the sun position" readme = "README.md" license = "Apache-2.0" From 14abe53beacc92842633c2be7fcb5e82ed16d104 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 01:31:32 -0700 Subject: [PATCH 3/6] docs: clarify YAML is optional for UI setup --- 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 6a2f024a..9bb2a67e 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.~~ This isn't true (anymore?). If you do add an entry to the YAML, the options page will stay empty and uneditable. Removing the entry and adding the integration via de UI, enables all options. +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 c05587a092f0f3aeb109d4d69bc2f5c77cbc9d74 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 01:31:45 -0700 Subject: [PATCH 4/6] Fix autoreset timeout during partial adaptation --- custom_components/adaptive_lighting/switch.py | 4 +- tests/test_switch.py | 81 ++++++++++++++++++- 2 files changed, 82 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..28e94492 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,82 @@ 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 + ) + + async def test_adaptation_attribute_selection(hass): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch(hass) From e6d45ab24135c47a277f13babecc877c1d4a56a2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 01:35:13 -0700 Subject: [PATCH 5/6] chore: align locked project version with prerelease --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 57564ac4..43c85eb6 100644 --- a/uv.lock +++ b/uv.lock @@ -71,7 +71,7 @@ wheels = [ [[package]] name = "adaptive-lighting" -version = "1.30.1" +version = "1.32.0b1" source = { editable = "." } [package.dev-dependencies] From fe43c1de2063d43d44e467f0905f535d0b6a2542 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 01:39:03 -0700 Subject: [PATCH 6/6] Test manual timeout renewal for mixed light requests --- tests/test_switch.py | 59 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/test_switch.py b/tests/test_switch.py index 28e94492..52b1dd27 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1042,6 +1042,65 @@ async def test_interval_adaptation_preserves_manual_control_timeout( ) +@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)