Add tested blueprints for sleep, schedules, and daylight

This commit is contained in:
Bas Nijholt 2026-09-06 10:49:54 -07:00
commit bc593ad935
6 changed files with 405 additions and 23 deletions

View file

@ -272,6 +272,17 @@ Replace every entity ID below with the IDs from your Home Assistant instance. Fr
Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`.
Four examples also have blueprints with selectors, so you can configure them without editing YAML:
| Blueprint | Purpose |
| --- | --- |
| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. |
| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. |
| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. |
| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. |
Copy a blueprint's link into **Settings → Automations & scenes → Blueprints → Import Blueprint**, then create an automation from it. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations.
`change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused.
<details markdown="1">
@ -294,6 +305,8 @@ This is a top-level `configuration.yaml` example. The timer clears manual contro
<details markdown="1">
<summary>Toggle multiple Adaptive Lighting switches to "sleep mode" using an <code>input_boolean.sleep_mode</code>.</summary>
Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml). Select an input boolean and the sleep-mode switches it should control.
```yaml
- alias: "Adaptive lighting: toggle 'sleep mode'"
trigger:
@ -392,6 +405,8 @@ script:
<details markdown="1">
<summary>Use a Schedule helper as a step-based custom lighting profile.</summary>
Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml). Select the main profile switch and your Schedule helper.
Create a [Schedule helper](https://www.home-assistant.io/integrations/schedule/) named `Adaptive Lighting Profile`. Add time blocks with Additional data like this:
```yaml
@ -441,6 +456,8 @@ This creates step changes at block boundaries. It does not interpolate between s
<details markdown="1">
<summary>Reduce daytime brightness when an illuminance sensor detects strong daylight.</summary>
Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml). Select the profile and sensor, then set the lux thresholds and brightness limits. The high lux threshold must exceed the low threshold; the blueprint does nothing if they are reversed or equal.
Keep a low configured `min_brightness` for late night and let an automation lower `max_brightness` while the room has ample daylight. Use a sensor that is not significantly affected by the controlled lights to avoid a feedback loop.
```yaml

View file

@ -0,0 +1,112 @@
blueprint:
name: "Adaptive Lighting: limit brightness in daylight"
description: >-
Lower a profile's maximum brightness in strong daylight and restore the
chosen normal maximum when daylight falls. Separate lux thresholds prevent
repeated changes near a single threshold. Use a sensor not significantly
affected by the controlled lights. Keep the daylight maximum at or above
the profile's minimum unless you want an inverted brightness curve.
Startup waits up to five minutes for a numeric sensor reading; a reading
between the thresholds leaves the configured maximum unchanged.
domain: automation
homeassistant:
min_version: "2025.9.0"
input:
adaptive_switch:
name: Adaptive Lighting profile
description: Select the main profile switch, not a sleep or adaptation switch.
selector:
entity:
filter:
domain: switch
integration: adaptive_lighting
illuminance_sensor:
name: Illuminance sensor
selector:
entity:
filter:
domain: sensor
device_class: illuminance
high_lux:
name: Strong daylight threshold
description: Must be greater than the low daylight threshold.
default: 300
selector:
number:
min: 0
max: 200000
mode: box
unit_of_measurement: lx
low_lux:
name: Low daylight threshold
default: 200
selector:
number:
min: 0
max: 200000
mode: box
unit_of_measurement: lx
daylight_maximum:
name: Maximum brightness in strong daylight
default: 30
selector:
number:
min: 1
max: 100
mode: box
unit_of_measurement: "%"
normal_maximum:
name: Normal maximum brightness
default: 100
selector:
number:
min: 1
max: 100
mode: box
unit_of_measurement: "%"
mode: restart
variables:
illuminance_sensor: !input illuminance_sensor
high_lux: !input high_lux
low_lux: !input low_lux
triggers:
- trigger: numeric_state
entity_id: !input illuminance_sensor
above: !input high_lux
- trigger: numeric_state
entity_id: !input illuminance_sensor
below: !input low_lux
- trigger: homeassistant
event: start
id: startup
conditions:
- condition: template
value_template: "{{ high_lux > low_lux }}"
actions:
- if:
- condition: trigger
id: startup
then:
- wait_template: "{{ is_number(states(illuminance_sensor)) }}"
timeout: "00:05:00"
continue_on_timeout: false
- choose:
- conditions:
- condition: numeric_state
entity_id: !input illuminance_sensor
above: !input high_lux
sequence:
- action: adaptive_lighting.change_switch_settings
data:
entity_id: !input adaptive_switch
max_brightness: !input daylight_maximum
- conditions:
- condition: numeric_state
entity_id: !input illuminance_sensor
below: !input low_lux
sequence:
- action: adaptive_lighting.change_switch_settings
data:
entity_id: !input adaptive_switch
max_brightness: !input normal_maximum

View file

@ -0,0 +1,57 @@
blueprint:
name: "Adaptive Lighting: scheduled profile"
description: >-
Apply fixed brightness and color temperature from a Schedule helper's
brightness_pct and color_temp_kelvin attributes. Uses step changes, not
interpolation. Missing attributes fall back to 1% and 2000 K. Outside an
active block, restores ALL configured profile settings. Use this only if
other automations do not also change runtime settings on this profile.
Reapplies the active block on Home Assistant startup. Updates settings
while the profile is off without enabling it; preserves manual control.
domain: automation
homeassistant:
min_version: "2025.9.0"
input:
adaptive_switch:
name: Adaptive Lighting profile
description: Select the main profile switch, not a sleep or adaptation switch.
selector:
entity:
filter:
domain: switch
integration: adaptive_lighting
schedule_entity:
name: Schedule helper
description: Add brightness_pct (1100) and color_temp_kelvin to each block's Additional data.
selector:
entity:
filter:
domain: schedule
mode: restart
variables:
schedule_entity: !input schedule_entity
triggers:
- trigger: state
entity_id: !input schedule_entity
- trigger: homeassistant
event: start
actions:
- choose:
- conditions:
- condition: state
entity_id: !input schedule_entity
state: "on"
sequence:
- action: adaptive_lighting.change_switch_settings
data:
entity_id: !input adaptive_switch
min_brightness: "{{ state_attr(schedule_entity, 'brightness_pct') | int(1) }}"
max_brightness: "{{ state_attr(schedule_entity, 'brightness_pct') | int(1) }}"
min_color_temp: "{{ state_attr(schedule_entity, 'color_temp_kelvin') | int(2000) }}"
max_color_temp: "{{ state_attr(schedule_entity, 'color_temp_kelvin') | int(2000) }}"
default:
- action: adaptive_lighting.change_switch_settings
data:
entity_id: !input adaptive_switch
use_defaults: configuration

View file

@ -0,0 +1,41 @@
blueprint:
name: "Adaptive Lighting: synchronize sleep mode"
description: >-
Keep the selected Adaptive Lighting sleep-mode switches in sync with an
input boolean, including its restored state at Home Assistant startup.
Unknown or unavailable helper states are ignored. Select only sleep-mode
switches, not the main profile or adaptation switches.
domain: automation
homeassistant:
min_version: "2025.9.0"
input:
sleep_helper:
name: Sleep-mode helper
selector:
entity:
filter:
domain: input_boolean
sleep_switches:
name: Adaptive Lighting sleep-mode switches
selector:
entity:
multiple: true
filter:
domain: switch
integration: adaptive_lighting
triggers:
- trigger: state
entity_id: !input sleep_helper
- trigger: homeassistant
event: start
variables:
sleep_helper: !input sleep_helper
sleep_mode: "{{ states(sleep_helper) }}"
conditions:
- condition: template
value_template: "{{ sleep_mode in ['on', 'off'] }}"
actions:
- action: "switch.turn_{{ sleep_mode }}"
target:
entity_id: !input sleep_switches

View file

@ -16,6 +16,17 @@ Replace every entity ID below with the IDs from your Home Assistant instance. Fr
Blocks that begin with `- alias` are entries for `automations.yaml`. Blocks with a top-level `script:` or `adaptive_lighting:` key are complete `configuration.yaml` examples. If your configuration uses `script: !include scripts.yaml`, omit that outer key and place its contents in `scripts.yaml`.
Four examples also have blueprints with selectors, so you can configure them without editing YAML:
| Blueprint | Purpose |
| --- | --- |
| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. |
| [Minimum brightness](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/turn_off_at_minimum.yaml) | Turn one light off when its target crosses down to the minimum. |
| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. |
| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. |
Copy a blueprint's link into **Settings → Automations & scenes → Blueprints → Import Blueprint**, then create an automation from it. Read the matching example below for setup and behavior. Each blueprint is tested through Home Assistant alongside its YAML example. The built-in manual-control timeout needs no automation; the scripts below remain useful as actions in your own automations.
`change_switch_settings` updates a profile while its main switch is off, but lights are adapted only while that switch is on. It preserves manual-control flags, so manually controlled lights remain paused.
<details markdown="1">
@ -38,6 +49,8 @@ This is a top-level `configuration.yaml` example. The timer clears manual contro
<details markdown="1">
<summary>Toggle multiple Adaptive Lighting switches to "sleep mode" using an <code>input_boolean.sleep_mode</code>.</summary>
Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml). Select an input boolean and the sleep-mode switches it should control.
```yaml
- alias: "Adaptive lighting: toggle 'sleep mode'"
trigger:
@ -136,6 +149,8 @@ script:
<details markdown="1">
<summary>Use a Schedule helper as a step-based custom lighting profile.</summary>
Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml). Select the main profile switch and your Schedule helper.
Create a [Schedule helper](https://www.home-assistant.io/integrations/schedule/) named `Adaptive Lighting Profile`. Add time blocks with Additional data like this:
```yaml
@ -185,6 +200,8 @@ This creates step changes at block boundaries. It does not interpolate between s
<details markdown="1">
<summary>Reduce daytime brightness when an illuminance sensor detects strong daylight.</summary>
Also available as a [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml). Select the profile and sensor, then set the lux thresholds and brightness limits. The high lux threshold must exceed the low threshold; the blueprint does nothing if they are reversed or equal.
Keep a low configured `min_brightness` for late night and let an automation lower `max_brightness` while the room has ample daylight. Use a sensor that is not significantly affected by the controlled lights to avoid a feedback loop.
```yaml

View file

@ -35,6 +35,7 @@ from homeassistant.components.adaptive_lighting.const import (
CONF_TRANSITION,
DOMAIN,
)
from homeassistant.components.blueprint.models import Blueprint
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
@ -56,6 +57,7 @@ from homeassistant.const import (
from homeassistant.core import CoreState, Event, HomeAssistant, State, callback
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from homeassistant.util import yaml as yaml_util
from tests.common import async_fire_time_changed
@ -150,6 +152,53 @@ def _prepare_hass_startup(hass: HomeAssistant) -> None:
hass.set_state(CoreState.not_running)
def _blueprint_config(hass, tmp_path, filename, inputs, alias):
"""Install an actual published blueprint for Home Assistant to load."""
relative_path = f"adaptive_lighting/{filename}"
hass.config.config_dir = str(tmp_path)
destination = tmp_path / "blueprints" / "automation" / relative_path
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(README.parent / "blueprints" / "automation" / filename, destination)
return {
"alias": alias,
"use_blueprint": {"path": relative_path, "input": inputs},
}
@pytest.fixture(params=["yaml", "blueprint"])
def published_automation(hass: HomeAssistant, tmp_path: Path, request):
"""Use the published YAML or blueprint with the same behavioral assertions."""
def config(summary, filename, inputs):
yaml_config = _yaml_documents(summary)[-1]
if request.param == "yaml":
return yaml_config
return _blueprint_config(
hass,
tmp_path,
filename,
inputs,
yaml_config[0]["alias"],
)
return config
@pytest.mark.parametrize(
"path",
sorted((README.parent / "blueprints" / "automation").glob("*.yaml")),
ids=lambda path: path.stem,
)
def test_published_blueprint_schema(path: Path) -> None:
"""Validate every published blueprint with Home Assistant's own schema."""
blueprint = Blueprint(
yaml_util.load_yaml(str(path)),
expected_domain=automation.DOMAIN,
schema=automation.config.AUTOMATION_BLUEPRINT_SCHEMA,
)
assert blueprint.validate() is None
@pytest.fixture(params=["yaml", "blueprint", "blueprint-custom-minimum"])
def minimum_automation_config(hass: HomeAssistant, tmp_path: Path, request):
"""Run the same behavior checks against both published formats."""
@ -157,14 +206,6 @@ def minimum_automation_config(hass: HomeAssistant, tmp_path: Path, request):
return _yaml_documents(
"Turn a light off when its adaptive brightness target reaches the minimum.",
)[0]
relative_path = "adaptive_lighting/turn_off_at_minimum.yaml"
hass.config.config_dir = str(tmp_path)
destination = tmp_path / "blueprints" / "automation" / relative_path
destination.parent.mkdir(parents=True)
shutil.copyfile(
README.parent / "blueprints" / "automation" / "turn_off_at_minimum.yaml",
destination,
)
inputs = {
"adaptive_switch": "switch.adaptive_lighting_living_room",
"brightness_switch": "switch.adaptive_lighting_living_room_adapt_brightness",
@ -172,13 +213,13 @@ def minimum_automation_config(hass: HomeAssistant, tmp_path: Path, request):
}
if request.param == "blueprint-custom-minimum":
inputs["minimum_pct"] = 10
return {
"alias": "Turn off at minimum",
"use_blueprint": {
"path": relative_path,
"input": inputs,
},
}
return _blueprint_config(
hass,
tmp_path,
"turn_off_at_minimum.yaml",
inputs,
"Turn off at minimum",
)
@pytest.mark.parametrize("manual_control", [False, True])
@ -340,10 +381,18 @@ async def test_minimum_brightness_ignores_missing_previous_target(
async def test_schedule_profile_executes_blocks_and_restore(
hass: HomeAssistant,
published_automation,
) -> None:
"""Catch ignored attribute changes, incomplete restore, or switch coupling."""
summary = "Use a Schedule helper as a step-based custom lighting profile."
automation_config = _yaml_documents(summary)[-1]
automation_config = published_automation(
summary,
"schedule_profile.yaml",
{
"adaptive_switch": "switch.adaptive_lighting_living_room",
"schedule_entity": "schedule.adaptive_lighting_profile",
},
)
_, adaptive_switch = await setup_switch(
hass,
{
@ -412,11 +461,21 @@ async def test_schedule_profile_executes_blocks_and_restore(
assert adaptive_switch._sun_light_settings.max_color_temp == 2750
async def test_schedule_profile_reapplies_at_startup(hass: HomeAssistant) -> None:
async def test_schedule_profile_reapplies_at_startup(
hass: HomeAssistant,
published_automation,
) -> None:
"""Verify startup applies the already-active schedule block."""
_prepare_hass_startup(hass)
summary = "Use a Schedule helper as a step-based custom lighting profile."
automation_config = _yaml_documents(summary)[-1]
automation_config = published_automation(
summary,
"schedule_profile.yaml",
{
"adaptive_switch": "switch.adaptive_lighting_living_room",
"schedule_entity": "schedule.adaptive_lighting_profile",
},
)
_, adaptive_switch = await setup_switch(hass, {CONF_NAME: "Living Room"})
hass.states.async_set(
"schedule.adaptive_lighting_profile",
@ -433,12 +492,22 @@ async def test_schedule_profile_reapplies_at_startup(hass: HomeAssistant) -> Non
assert adaptive_switch._sun_light_settings.max_color_temp == 2500
async def test_lux_profile_executes_hysteresis(hass: HomeAssistant) -> None:
async def test_lux_profile_executes_hysteresis(
hass: HomeAssistant,
published_automation,
) -> None:
"""Catch missing threshold actions or changes inside the dead band."""
summary = (
"Reduce daytime brightness when an illuminance sensor detects strong daylight."
)
automation_config = _yaml_documents(summary)[0]
automation_config = published_automation(
summary,
"daylight_limit.yaml",
{
"adaptive_switch": "switch.adaptive_lighting_living_room",
"illuminance_sensor": "sensor.living_room_illuminance",
},
)
_, adaptive_switch = await setup_switch(
hass,
{CONF_NAME: "Living Room", CONF_MAX_BRIGHTNESS: 80},
@ -465,13 +534,21 @@ async def test_lux_profile_executes_hysteresis(hass: HomeAssistant) -> None:
async def test_lux_profile_executes_unknown_recovery_at_startup(
hass: HomeAssistant,
published_automation,
) -> None:
"""Catch a startup hang or failure to recover from an unknown sensor."""
_prepare_hass_startup(hass)
summary = (
"Reduce daytime brightness when an illuminance sensor detects strong daylight."
)
automation_config = _yaml_documents(summary)[0]
automation_config = published_automation(
summary,
"daylight_limit.yaml",
{
"adaptive_switch": "switch.adaptive_lighting_living_room",
"illuminance_sensor": "sensor.living_room_illuminance",
},
)
_, adaptive_switch = await setup_switch(
hass,
{CONF_NAME: "Living Room", CONF_MAX_BRIGHTNESS: 80},
@ -494,6 +571,44 @@ async def test_lux_profile_executes_unknown_recovery_at_startup(
assert adaptive_switch._sun_light_settings.max_brightness == 30
@pytest.mark.parametrize(("high_lux", "low_lux"), [(400, 250), (200, 300), (200, 200)])
async def test_daylight_blueprint_custom_inputs(
hass: HomeAssistant,
tmp_path: Path,
high_lux: int,
low_lux: int,
) -> None:
"""Use selected entities and limits; invalid threshold order must do nothing."""
config = _blueprint_config(
hass,
tmp_path,
"daylight_limit.yaml",
{
"adaptive_switch": "switch.adaptive_lighting_office",
"illuminance_sensor": "sensor.office_illuminance",
"high_lux": high_lux,
"low_lux": low_lux,
"daylight_maximum": 20,
"normal_maximum": 70,
},
"Custom daylight",
)
_, adaptive_switch = await setup_switch(
hass,
{CONF_NAME: "Office", CONF_MAX_BRIGHTNESS: 80},
)
hass.states.async_set("sensor.office_illuminance", "300")
await _setup_automation(hass, config)
assert hass.states.get("automation.custom_daylight") is not None
valid_thresholds = high_lux > low_lux
for lux, expected in [(500, 20), (300, 20), (100, 70)]:
hass.states.async_set("sensor.office_illuminance", str(lux))
await hass.async_block_till_done()
assert adaptive_switch._sun_light_settings.max_brightness == (
expected if valid_thresholds else 80
)
async def test_hue_script_applies_current_values_to_fresh_profile_targets(
hass: HomeAssistant,
) -> None:
@ -766,13 +881,24 @@ async def test_autoreset_manual_control_uses_one_renewable_timer(
async def test_sleep_toggle_uses_fresh_profile_entity_ids(
hass: HomeAssistant,
published_automation,
) -> None:
"""Execute state triggers against fresh child entity IDs."""
summary = (
'Toggle multiple Adaptive Lighting switches to "sleep mode" using an '
"<code>input_boolean.sleep_mode</code>."
)
automation_config = _yaml_documents(summary)[0]
automation_config = published_automation(
summary,
"sleep_mode.yaml",
{
"sleep_helper": "input_boolean.sleep_mode",
"sleep_switches": [
"switch.adaptive_lighting_living_room_sleep_mode",
"switch.adaptive_lighting_bedroom_sleep_mode",
],
},
)
assert await async_setup_component(
hass,
"input_boolean",
@ -815,6 +941,7 @@ async def test_sleep_toggle_uses_fresh_profile_entity_ids(
async def test_sleep_toggle_applies_restored_state_at_startup(
hass: HomeAssistant,
published_automation,
) -> None:
"""Verify startup applies the input boolean's restored state."""
_prepare_hass_startup(hass)
@ -822,13 +949,24 @@ async def test_sleep_toggle_applies_restored_state_at_startup(
'Toggle multiple Adaptive Lighting switches to "sleep mode" using an '
"<code>input_boolean.sleep_mode</code>."
)
automation_config = _yaml_documents(summary)[0]
automation_config = published_automation(
summary,
"sleep_mode.yaml",
{
"sleep_helper": "input_boolean.sleep_mode",
"sleep_switches": [
"switch.adaptive_lighting_living_room_sleep_mode",
"switch.adaptive_lighting_bedroom_sleep_mode",
],
},
)
assert await async_setup_component(
hass,
"input_boolean",
{"input_boolean": {"sleep_mode": {}}},
)
await setup_switch(hass, {CONF_NAME: "Living Room"})
await setup_switch(hass, {CONF_NAME: "Bedroom"})
await hass.services.async_call(
"input_boolean",
SERVICE_TURN_ON,