mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 22:34:04 +02:00
Pause brightness at minimum using existing manual-control resets (#1578)
This commit is contained in:
parent
f7b50b12d9
commit
11bd5cf2aa
4 changed files with 617 additions and 16 deletions
106
README.md
106
README.md
|
|
@ -272,16 +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:
|
||||
Five 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. |
|
||||
| Blueprint | Purpose | Import |
|
||||
| --- | --- | --- |
|
||||
| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fsleep_mode.yaml) |
|
||||
| [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. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fturn_off_at_minimum.yaml) |
|
||||
| [Pause at minimum](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) | Pause brightness through manual control, using its existing reset behavior. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fmanual_control_at_minimum.yaml) |
|
||||
| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fschedule_profile.yaml) |
|
||||
| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fdaylight_limit.yaml) |
|
||||
|
||||
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.
|
||||
Click a blueprint's import badge, confirm the import in Home Assistant, then create an automation and select your entities. You can also copy its source link into **Settings → Automations & scenes → Blueprints → Import Blueprint**. 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.
|
||||
|
||||
|
|
@ -379,6 +380,95 @@ This runs once when a valid target crosses down into the minimum range. It skips
|
|||
|
||||
</details>
|
||||
|
||||
<details markdown="1">
|
||||
<summary>Pause brightness at the minimum using manual control.</summary>
|
||||
|
||||
Use the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) to mark an individual light's brightness as manually controlled when the calculated target reaches its minimum. The light stays on and the adaptation switches stay enabled. Set `take_over_control_mode: pause_changed` on the profile to keep adapting color; the default `pause_all` pauses both attributes.
|
||||
|
||||
Select the profile, its adapt-brightness switch, and a light managed by it. Match the minimum percentage to the profile's `min_brightness`. This YAML example assumes `min_brightness: 1`; change `minimum_pct` and the entity IDs to match your setup.
|
||||
|
||||
```yaml
|
||||
- alias: "Adaptive lighting: pause brightness at minimum"
|
||||
mode: single
|
||||
variables:
|
||||
minimum_pct: 1
|
||||
minimum: "{{ (minimum_pct * 255 / 100) | round(0) }}"
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: switch.adaptive_lighting_living_room
|
||||
attribute: brightness_pct
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id:
|
||||
- switch.adaptive_lighting_living_room
|
||||
- switch.adaptive_lighting_living_room_adapt_brightness
|
||||
state: "on"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set before = trigger.from_state.attributes.get('brightness_pct')
|
||||
if trigger.from_state else none %}
|
||||
{% set after = trigger.to_state.attributes.get('brightness_pct')
|
||||
if trigger.to_state else none %}
|
||||
{{ is_number(before) and is_number(after)
|
||||
and (before | float * 255 / 100) | round(0) > minimum
|
||||
and (after | float * 255 / 100) | round(0) <= minimum }}
|
||||
- condition: state
|
||||
entity_id: light.living_room
|
||||
state: "on"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ 'light.living_room' not in
|
||||
(state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }}
|
||||
actions:
|
||||
- variables:
|
||||
light_session: "{{ states.light.living_room.last_changed.isoformat() }}"
|
||||
profile_session: "{{ states.switch.adaptive_lighting_living_room.last_changed.isoformat() }}"
|
||||
brightness_session: "{{ states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() }}"
|
||||
- wait_template: >-
|
||||
{% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %}
|
||||
{{ not is_state('light.living_room', 'on')
|
||||
or not is_state('switch.adaptive_lighting_living_room', 'on')
|
||||
or not is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on')
|
||||
or states.light.living_room.last_changed.isoformat() != light_session
|
||||
or states.switch.adaptive_lighting_living_room.last_changed.isoformat() != profile_session
|
||||
or states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() != brightness_session
|
||||
or not is_number(target) or (target | float * 255 / 100) | round(0) > minimum
|
||||
or 'light.living_room' in
|
||||
(state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or [])
|
||||
or (state_attr('light.living_room', 'brightness') | float(256)) <= minimum }}
|
||||
timeout: "00:05:00"
|
||||
continue_on_timeout: false
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %}
|
||||
{{ is_state('light.living_room', 'on')
|
||||
and is_state('switch.adaptive_lighting_living_room', 'on')
|
||||
and is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on')
|
||||
and states.light.living_room.last_changed.isoformat() == light_session
|
||||
and states.switch.adaptive_lighting_living_room.last_changed.isoformat() == profile_session
|
||||
and states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() == brightness_session
|
||||
and is_number(target) and (target | float * 255 / 100) | round(0) <= minimum
|
||||
and (state_attr('light.living_room', 'brightness') | float(256)) <= minimum
|
||||
and 'light.living_room' not in
|
||||
(state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }}
|
||||
- action: adaptive_lighting.set_manual_control
|
||||
data:
|
||||
entity_id: switch.adaptive_lighting_living_room
|
||||
lights: light.living_room
|
||||
manual_control: >-
|
||||
{{ true if 'light.living_room' in
|
||||
(state_attr('switch.adaptive_lighting_living_room', 'manual_control_color') or [])
|
||||
else 'brightness' }}
|
||||
```
|
||||
|
||||
The comparison uses the rounded 0–255 target, so it does not depend on sampling an exact floating-point minimum. It waits up to five minutes for the light to report that minimum before marking manual control, so the final dimming command can complete. Reported brightness does not prove physical fade completion. If the light, profile, or adapt-brightness switch is toggled, the target rises, brightness is marked manually controlled elsewhere, or brightness never reaches the minimum, the attempt is abandoned. Lights that cannot report the configured minimum will not be paused. Existing manual color flags are preserved, and lights whose brightness is already manually controlled are left alone. Manual-control state is shared for lights managed by multiple profiles, so their existing takeover policies still apply.
|
||||
|
||||
The usual resets apply: turning the light off, the configured `autoreset_control_seconds` timeout, clearing manual control through its service, and existing profile/sleep-switch reset behavior. After a reset, normal adaptation can increase brightness again. This runs once per downward crossing; resetting while the target remains at its minimum does not immediately mark the light again. Startup at the minimum is not a crossing either.
|
||||
|
||||
This pauses further dimming as well as brightening. To pause brightness immediately after a brightness change made through Home Assistant, use `take_over_control_mode: pause_changed` with `take_over_control: true`; that needs no additional automation.
|
||||
|
||||
</details>
|
||||
|
||||
<details markdown="1">
|
||||
<summary>Set sunrise and sunset from an alarm.</summary>
|
||||
|
||||
|
|
|
|||
123
blueprints/automation/manual_control_at_minimum.yaml
Normal file
123
blueprints/automation/manual_control_at_minimum.yaml
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
blueprint:
|
||||
name: "Adaptive Lighting: pause brightness at minimum"
|
||||
description: >-
|
||||
Mark one light's brightness as manually controlled when its calculated
|
||||
brightness target crosses down to the configured minimum. Set the profile's
|
||||
take_over_control_mode to pause_changed to keep adapting color; pause_all
|
||||
pauses both. Existing manual color control is preserved. Uses the normal
|
||||
manual-control resets, including turning the light off and any configured
|
||||
autoreset_control_seconds timeout. Runs once per downward crossing, so a
|
||||
reset while the target remains low does not immediately pause it again.
|
||||
Waits up to five minutes for reported brightness to reach the rounded
|
||||
0–255 minimum before pausing. This does not prove physical fade completion. Does not
|
||||
change power or adaptation switches. Select an individual light managed by
|
||||
the profile. Shared profiles retain their usual manual-control behavior.
|
||||
domain: automation
|
||||
homeassistant:
|
||||
min_version: "2025.9.0"
|
||||
input:
|
||||
adaptive_switch:
|
||||
name: Adaptive Lighting profile
|
||||
description: Select the main Adaptive Lighting switch, not a sleep or adaptation switch.
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain: switch
|
||||
integration: adaptive_lighting
|
||||
brightness_switch:
|
||||
name: Adapt brightness switch
|
||||
description: Select the adapt brightness switch belonging to the same profile.
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain: switch
|
||||
integration: adaptive_lighting
|
||||
light_entity:
|
||||
name: Light
|
||||
description: Select one light managed by this profile. Create an automation for each light.
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain: light
|
||||
minimum_pct:
|
||||
name: Minimum brightness
|
||||
description: Match the profile's min_brightness setting. Update this if that setting changes.
|
||||
default: 1
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 100
|
||||
step: 1
|
||||
unit_of_measurement: "%"
|
||||
mode: box
|
||||
|
||||
mode: single
|
||||
variables:
|
||||
adaptive_switch: !input adaptive_switch
|
||||
light_entity: !input light_entity
|
||||
minimum_pct: !input minimum_pct
|
||||
minimum: "{{ (minimum_pct * 255 / 100) | round(0) }}"
|
||||
brightness_switch: !input brightness_switch
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: !input adaptive_switch
|
||||
attribute: brightness_pct
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id: !input adaptive_switch
|
||||
state: "on"
|
||||
- condition: state
|
||||
entity_id: !input brightness_switch
|
||||
state: "on"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set before = trigger.from_state.attributes.get('brightness_pct')
|
||||
if trigger.from_state else none %}
|
||||
{% set after = trigger.to_state.attributes.get('brightness_pct')
|
||||
if trigger.to_state else none %}
|
||||
{{ is_number(before) and is_number(after)
|
||||
and (before | float * 255 / 100) | round(0) > minimum
|
||||
and (after | float * 255 / 100) | round(0) <= minimum }}
|
||||
- condition: state
|
||||
entity_id: !input light_entity
|
||||
state: "on"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ light_entity not in (state_attr(adaptive_switch, 'manual_control_brightness') or []) }}
|
||||
actions:
|
||||
- variables:
|
||||
light_session: "{{ states[light_entity].last_changed.isoformat() }}"
|
||||
profile_session: "{{ states[adaptive_switch].last_changed.isoformat() }}"
|
||||
brightness_session: "{{ states[brightness_switch].last_changed.isoformat() }}"
|
||||
- wait_template: >-
|
||||
{% set target = state_attr(adaptive_switch, 'brightness_pct') %}
|
||||
{{ not is_state(light_entity, 'on') or not is_state(adaptive_switch, 'on')
|
||||
or not is_state(brightness_switch, 'on')
|
||||
or states[light_entity].last_changed.isoformat() != light_session
|
||||
or states[adaptive_switch].last_changed.isoformat() != profile_session
|
||||
or states[brightness_switch].last_changed.isoformat() != brightness_session
|
||||
or not is_number(target) or (target | float * 255 / 100) | round(0) > minimum
|
||||
or light_entity in (state_attr(adaptive_switch, 'manual_control_brightness') or [])
|
||||
or (state_attr(light_entity, 'brightness') | float(256)) <= minimum }}
|
||||
timeout: "00:05:00"
|
||||
continue_on_timeout: false
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set target = state_attr(adaptive_switch, 'brightness_pct') %}
|
||||
{{ is_state(light_entity, 'on') and is_state(adaptive_switch, 'on')
|
||||
and is_state(brightness_switch, 'on')
|
||||
and states[light_entity].last_changed.isoformat() == light_session
|
||||
and states[adaptive_switch].last_changed.isoformat() == profile_session
|
||||
and states[brightness_switch].last_changed.isoformat() == brightness_session
|
||||
and is_number(target) and (target | float * 255 / 100) | round(0) <= minimum
|
||||
and (state_attr(light_entity, 'brightness') | float(256)) <= minimum
|
||||
and light_entity not in
|
||||
(state_attr(adaptive_switch, 'manual_control_brightness') or []) }}
|
||||
- action: adaptive_lighting.set_manual_control
|
||||
data:
|
||||
entity_id: !input adaptive_switch
|
||||
lights: !input light_entity
|
||||
manual_control: >-
|
||||
{{ true if light_entity in
|
||||
(state_attr(adaptive_switch, 'manual_control_color') or [])
|
||||
else 'brightness' }}
|
||||
|
|
@ -16,16 +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:
|
||||
Five 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. |
|
||||
| Blueprint | Purpose | Import |
|
||||
| --- | --- | --- |
|
||||
| [Sleep mode](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/sleep_mode.yaml) | Synchronize several profiles with one sleep-mode helper. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fsleep_mode.yaml) |
|
||||
| [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. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fturn_off_at_minimum.yaml) |
|
||||
| [Pause at minimum](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) | Pause brightness through manual control, using its existing reset behavior. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fmanual_control_at_minimum.yaml) |
|
||||
| [Schedule profile](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/schedule_profile.yaml) | Apply brightness and color temperature from Schedule helper blocks. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fschedule_profile.yaml) |
|
||||
| [Daylight limit](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/daylight_limit.yaml) | Lower maximum brightness in strong daylight. | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgithub.com%2Fbasnijholt%2Fadaptive-lighting%2Fblob%2Fmain%2Fblueprints%2Fautomation%2Fdaylight_limit.yaml) |
|
||||
|
||||
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.
|
||||
Click a blueprint's import badge, confirm the import in Home Assistant, then create an automation and select your entities. You can also copy its source link into **Settings → Automations & scenes → Blueprints → Import Blueprint**. 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.
|
||||
|
||||
|
|
@ -123,6 +124,95 @@ This runs once when a valid target crosses down into the minimum range. It skips
|
|||
|
||||
</details>
|
||||
|
||||
<details markdown="1">
|
||||
<summary>Pause brightness at the minimum using manual control.</summary>
|
||||
|
||||
Use the [blueprint](https://github.com/basnijholt/adaptive-lighting/blob/main/blueprints/automation/manual_control_at_minimum.yaml) to mark an individual light's brightness as manually controlled when the calculated target reaches its minimum. The light stays on and the adaptation switches stay enabled. Set `take_over_control_mode: pause_changed` on the profile to keep adapting color; the default `pause_all` pauses both attributes.
|
||||
|
||||
Select the profile, its adapt-brightness switch, and a light managed by it. Match the minimum percentage to the profile's `min_brightness`. This YAML example assumes `min_brightness: 1`; change `minimum_pct` and the entity IDs to match your setup.
|
||||
|
||||
```yaml
|
||||
- alias: "Adaptive lighting: pause brightness at minimum"
|
||||
mode: single
|
||||
variables:
|
||||
minimum_pct: 1
|
||||
minimum: "{{ (minimum_pct * 255 / 100) | round(0) }}"
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: switch.adaptive_lighting_living_room
|
||||
attribute: brightness_pct
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id:
|
||||
- switch.adaptive_lighting_living_room
|
||||
- switch.adaptive_lighting_living_room_adapt_brightness
|
||||
state: "on"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set before = trigger.from_state.attributes.get('brightness_pct')
|
||||
if trigger.from_state else none %}
|
||||
{% set after = trigger.to_state.attributes.get('brightness_pct')
|
||||
if trigger.to_state else none %}
|
||||
{{ is_number(before) and is_number(after)
|
||||
and (before | float * 255 / 100) | round(0) > minimum
|
||||
and (after | float * 255 / 100) | round(0) <= minimum }}
|
||||
- condition: state
|
||||
entity_id: light.living_room
|
||||
state: "on"
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ 'light.living_room' not in
|
||||
(state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }}
|
||||
actions:
|
||||
- variables:
|
||||
light_session: "{{ states.light.living_room.last_changed.isoformat() }}"
|
||||
profile_session: "{{ states.switch.adaptive_lighting_living_room.last_changed.isoformat() }}"
|
||||
brightness_session: "{{ states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() }}"
|
||||
- wait_template: >-
|
||||
{% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %}
|
||||
{{ not is_state('light.living_room', 'on')
|
||||
or not is_state('switch.adaptive_lighting_living_room', 'on')
|
||||
or not is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on')
|
||||
or states.light.living_room.last_changed.isoformat() != light_session
|
||||
or states.switch.adaptive_lighting_living_room.last_changed.isoformat() != profile_session
|
||||
or states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() != brightness_session
|
||||
or not is_number(target) or (target | float * 255 / 100) | round(0) > minimum
|
||||
or 'light.living_room' in
|
||||
(state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or [])
|
||||
or (state_attr('light.living_room', 'brightness') | float(256)) <= minimum }}
|
||||
timeout: "00:05:00"
|
||||
continue_on_timeout: false
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{% set target = state_attr('switch.adaptive_lighting_living_room', 'brightness_pct') %}
|
||||
{{ is_state('light.living_room', 'on')
|
||||
and is_state('switch.adaptive_lighting_living_room', 'on')
|
||||
and is_state('switch.adaptive_lighting_living_room_adapt_brightness', 'on')
|
||||
and states.light.living_room.last_changed.isoformat() == light_session
|
||||
and states.switch.adaptive_lighting_living_room.last_changed.isoformat() == profile_session
|
||||
and states.switch.adaptive_lighting_living_room_adapt_brightness.last_changed.isoformat() == brightness_session
|
||||
and is_number(target) and (target | float * 255 / 100) | round(0) <= minimum
|
||||
and (state_attr('light.living_room', 'brightness') | float(256)) <= minimum
|
||||
and 'light.living_room' not in
|
||||
(state_attr('switch.adaptive_lighting_living_room', 'manual_control_brightness') or []) }}
|
||||
- action: adaptive_lighting.set_manual_control
|
||||
data:
|
||||
entity_id: switch.adaptive_lighting_living_room
|
||||
lights: light.living_room
|
||||
manual_control: >-
|
||||
{{ true if 'light.living_room' in
|
||||
(state_attr('switch.adaptive_lighting_living_room', 'manual_control_color') or [])
|
||||
else 'brightness' }}
|
||||
```
|
||||
|
||||
The comparison uses the rounded 0–255 target, so it does not depend on sampling an exact floating-point minimum. It waits up to five minutes for the light to report that minimum before marking manual control, so the final dimming command can complete. Reported brightness does not prove physical fade completion. If the light, profile, or adapt-brightness switch is toggled, the target rises, brightness is marked manually controlled elsewhere, or brightness never reaches the minimum, the attempt is abandoned. Lights that cannot report the configured minimum will not be paused. Existing manual color flags are preserved, and lights whose brightness is already manually controlled are left alone. Manual-control state is shared for lights managed by multiple profiles, so their existing takeover policies still apply.
|
||||
|
||||
The usual resets apply: turning the light off, the configured `autoreset_control_seconds` timeout, clearing manual control through its service, and existing profile/sleep-switch reset behavior. After a reset, normal adaptation can increase brightness again. This runs once per downward crossing; resetting while the target remains at its minimum does not immediately mark the light again. Startup at the minimum is not a crossing either.
|
||||
|
||||
This pauses further dimming as well as brightening. To pause brightness immediately after a brightness change made through Home Assistant, use `take_over_control_mode: pause_changed` with `take_over_control: true`; that needs no additional automation.
|
||||
|
||||
</details>
|
||||
|
||||
<details markdown="1">
|
||||
<summary>Set sunrise and sunset from an alarm.</summary>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from homeassistant.components.adaptive_lighting.adaptation_utils import (
|
|||
LightControlAttributes,
|
||||
)
|
||||
from homeassistant.components.adaptive_lighting.const import (
|
||||
CONF_AUTORESET_CONTROL,
|
||||
CONF_BRIGHTNESS_MODE,
|
||||
CONF_BRIGHTNESS_MODE_TIME_DARK,
|
||||
CONF_BRIGHTNESS_MODE_TIME_LIGHT,
|
||||
|
|
@ -32,6 +33,7 @@ from homeassistant.components.adaptive_lighting.const import (
|
|||
CONF_SLEEP_RGB_OR_COLOR_TEMP,
|
||||
CONF_SUNRISE_TIME,
|
||||
CONF_SUNSET_TIME,
|
||||
CONF_TAKE_OVER_CONTROL_MODE,
|
||||
CONF_TRANSITION,
|
||||
DOMAIN,
|
||||
)
|
||||
|
|
@ -379,6 +381,302 @@ async def test_minimum_brightness_ignores_missing_previous_target(
|
|||
assert hass.states.get("light.living_room").state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.parametrize("previous_manual", [None, "color", "brightness"])
|
||||
@patch(
|
||||
"homeassistant.components.adaptive_lighting.color_and_brightness.utcnow",
|
||||
new=dt_util.utcnow,
|
||||
)
|
||||
async def test_minimum_manual_control_lifecycle(
|
||||
hass: HomeAssistant,
|
||||
freezer,
|
||||
published_automation,
|
||||
previous_manual: str | None,
|
||||
) -> None:
|
||||
"""Pause at the calculated floor, preserve other flags, and reset on off/on."""
|
||||
freezer.move_to(datetime(2026, 9, 6, 18, 58, tzinfo=dt_util.DEFAULT_TIME_ZONE))
|
||||
config = published_automation(
|
||||
"Pause brightness at the minimum using manual control.",
|
||||
"manual_control_at_minimum.yaml",
|
||||
{
|
||||
"adaptive_switch": "switch.adaptive_lighting_living_room",
|
||||
"brightness_switch": "switch.adaptive_lighting_living_room_adapt_brightness",
|
||||
"light_entity": "light.living_room",
|
||||
},
|
||||
)
|
||||
await _setup_template_lights(hass, ["Living Room"])
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "light.living_room", ATTR_BRIGHTNESS: 77},
|
||||
blocking=True,
|
||||
)
|
||||
_, profile = await setup_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_NAME: "Living Room",
|
||||
CONF_LIGHTS: ["light.living_room"],
|
||||
CONF_MIN_BRIGHTNESS: 1,
|
||||
CONF_MAX_BRIGHTNESS: 100,
|
||||
CONF_MIN_COLOR_TEMP: 3000,
|
||||
CONF_MAX_COLOR_TEMP: 3000,
|
||||
CONF_BRIGHTNESS_MODE: "linear",
|
||||
CONF_BRIGHTNESS_MODE_TIME_DARK: timedelta(hours=1),
|
||||
CONF_BRIGHTNESS_MODE_TIME_LIGHT: timedelta(hours=1),
|
||||
CONF_SUNRISE_TIME: "06:00:00",
|
||||
CONF_SUNSET_TIME: "18:00:00",
|
||||
CONF_TRANSITION: 0,
|
||||
CONF_INITIAL_TRANSITION: 0,
|
||||
CONF_TAKE_OVER_CONTROL_MODE: "pause_changed",
|
||||
},
|
||||
)
|
||||
if previous_manual:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_manual_control",
|
||||
{ATTR_ENTITY_ID: profile.entity_id, "manual_control": previous_manual},
|
||||
blocking=True,
|
||||
)
|
||||
await _setup_automation(hass, config)
|
||||
manual_calls = []
|
||||
|
||||
@callback
|
||||
def record_manual_call(event: Event) -> None:
|
||||
if (
|
||||
event.data["domain"] == DOMAIN
|
||||
and event.data["service"] == "set_manual_control"
|
||||
):
|
||||
manual_calls.append(event.data["service_data"])
|
||||
|
||||
hass.bus.async_listen(EVENT_CALL_SERVICE, record_manual_call)
|
||||
freezer.move_to(datetime(2026, 9, 6, 18, 59, 50, tzinfo=dt_util.DEFAULT_TIME_ZONE))
|
||||
await profile._async_update_at_interval_action()
|
||||
await hass.async_block_till_done()
|
||||
flags = profile.manager.get_manual_control_attributes("light.living_room")
|
||||
assert LightControlAttributes.BRIGHTNESS in flags
|
||||
assert (LightControlAttributes.COLOR in flags) is (previous_manual == "color")
|
||||
assert len(manual_calls) == (0 if previous_manual == "brightness" else 1)
|
||||
paused_brightness = hass.states.get("light.living_room").attributes[ATTR_BRIGHTNESS]
|
||||
assert profile.is_on
|
||||
assert profile.adapt_brightness_switch.is_on
|
||||
if previous_manual != "brightness":
|
||||
assert paused_brightness == 3
|
||||
|
||||
freezer.move_to(datetime(2026, 9, 6, 19, 1, tzinfo=dt_util.DEFAULT_TIME_ZONE))
|
||||
await profile._async_update_at_interval_action()
|
||||
await hass.async_block_till_done()
|
||||
assert len(manual_calls) == (0 if previous_manual == "brightness" else 1)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"change_switch_settings",
|
||||
{
|
||||
ATTR_ENTITY_ID: profile.entity_id,
|
||||
CONF_MIN_BRIGHTNESS: 100,
|
||||
CONF_MAX_BRIGHTNESS: 100,
|
||||
CONF_MIN_COLOR_TEMP: 5000,
|
||||
CONF_MAX_COLOR_TEMP: 5000,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.living_room")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == paused_brightness
|
||||
assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx(
|
||||
3000 if previous_manual == "color" else 5000,
|
||||
abs=5,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "light.living_room"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert not profile.manager.get_manual_control_attributes("light.living_room")
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "light.living_room"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("light.living_room").attributes[ATTR_BRIGHTNESS] == 255
|
||||
|
||||
# Another crossing can pause again, but clearing it at the floor must stick.
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"change_switch_settings",
|
||||
{
|
||||
ATTR_ENTITY_ID: profile.entity_id,
|
||||
CONF_MIN_BRIGHTNESS: 1,
|
||||
CONF_MAX_BRIGHTNESS: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert profile.manager.get_manual_control_attributes("light.living_room")
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"set_manual_control",
|
||||
{ATTR_ENTITY_ID: profile.entity_id, "manual_control": False},
|
||||
blocking=True,
|
||||
)
|
||||
await profile._async_update_at_interval_action()
|
||||
await hass.async_block_till_done()
|
||||
assert not profile.manager.get_manual_control_attributes("light.living_room")
|
||||
|
||||
if previous_manual is None:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"change_switch_settings",
|
||||
{
|
||||
ATTR_ENTITY_ID: profile.entity_id,
|
||||
CONF_MIN_BRIGHTNESS: 50,
|
||||
CONF_MAX_BRIGHTNESS: 50,
|
||||
CONF_AUTORESET_CONTROL: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"change_switch_settings",
|
||||
{
|
||||
ATTR_ENTITY_ID: profile.entity_id,
|
||||
CONF_MIN_BRIGHTNESS: 1,
|
||||
CONF_MAX_BRIGHTNESS: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert profile.manager.get_manual_control_attributes("light.living_room")
|
||||
cleared, remove_listener = _state_waiter(
|
||||
hass,
|
||||
profile.entity_id,
|
||||
lambda state: state.attributes.get("manual_control_brightness") == [],
|
||||
)
|
||||
freezer.tick(timedelta(seconds=1))
|
||||
async_fire_time_changed(hass, dt_util.utcnow())
|
||||
await asyncio.wait_for(cleared, timeout=2)
|
||||
remove_listener()
|
||||
await hass.async_block_till_done()
|
||||
assert not profile.manager.get_manual_control_attributes("light.living_room")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"abort_entity",
|
||||
[
|
||||
"light.living_room",
|
||||
"switch.adaptive_lighting_living_room",
|
||||
"switch.adaptive_lighting_living_room_adapt_brightness",
|
||||
],
|
||||
)
|
||||
async def test_minimum_manual_control_aborts_when_disabled(
|
||||
hass: HomeAssistant,
|
||||
published_automation,
|
||||
abort_entity: str,
|
||||
) -> None:
|
||||
"""Abandon a pending wait immediately when the light or profile is disabled."""
|
||||
config = published_automation(
|
||||
"Pause brightness at the minimum using manual control.",
|
||||
"manual_control_at_minimum.yaml",
|
||||
{
|
||||
"adaptive_switch": "switch.adaptive_lighting_living_room",
|
||||
"brightness_switch": "switch.adaptive_lighting_living_room_adapt_brightness",
|
||||
"light_entity": "light.living_room",
|
||||
},
|
||||
)
|
||||
await _setup_template_lights(hass, ["Living Room"])
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "light.living_room", ATTR_BRIGHTNESS: 128},
|
||||
blocking=True,
|
||||
)
|
||||
_, profile = await setup_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_NAME: "Living Room",
|
||||
CONF_LIGHTS: ["light.living_room"],
|
||||
CONF_MIN_BRIGHTNESS: 50,
|
||||
CONF_MAX_BRIGHTNESS: 50,
|
||||
CONF_INITIAL_TRANSITION: 0,
|
||||
CONF_TRANSITION: 0,
|
||||
CONF_TAKE_OVER_CONTROL_MODE: "pause_changed",
|
||||
},
|
||||
)
|
||||
await _setup_automation(hass, config)
|
||||
waiting, remove_listener = _state_waiter(
|
||||
hass,
|
||||
"automation.adaptive_lighting_pause_brightness_at_minimum",
|
||||
lambda state: state.attributes.get("current") == 1,
|
||||
)
|
||||
state = hass.states.get(profile.entity_id)
|
||||
hass.states.async_set(
|
||||
profile.entity_id,
|
||||
STATE_ON,
|
||||
{**state.attributes, "brightness_pct": 1},
|
||||
)
|
||||
await asyncio.wait_for(waiting, timeout=1)
|
||||
remove_listener()
|
||||
assert not profile.manager.get_manual_control_attributes("light.living_room")
|
||||
stopped, remove_stopped = _state_waiter(
|
||||
hass,
|
||||
"automation.adaptive_lighting_pause_brightness_at_minimum",
|
||||
lambda state: state.attributes.get("current") == 0,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
abort_entity.split(".", maxsplit=1)[0],
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: abort_entity},
|
||||
blocking=True,
|
||||
)
|
||||
try:
|
||||
await asyncio.wait_for(stopped, timeout=1)
|
||||
finally:
|
||||
remove_stopped()
|
||||
if stopped.cancelled():
|
||||
await hass.services.async_call(
|
||||
automation.DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{
|
||||
ATTR_ENTITY_ID: "automation.adaptive_lighting_pause_brightness_at_minimum",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert not profile.manager.get_manual_control_attributes("light.living_room")
|
||||
assert (
|
||||
hass.states.get(
|
||||
"automation.adaptive_lighting_pause_brightness_at_minimum",
|
||||
).attributes["current"]
|
||||
== 0
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
abort_entity.split(".", maxsplit=1)[0],
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: abort_entity},
|
||||
blocking=True,
|
||||
)
|
||||
await profile._async_update_at_interval_action()
|
||||
await hass.async_block_till_done()
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"change_switch_settings",
|
||||
{
|
||||
ATTR_ENTITY_ID: profile.entity_id,
|
||||
CONF_MIN_BRIGHTNESS: 1,
|
||||
CONF_MAX_BRIGHTNESS: 1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
profile.manager.get_manual_control_attributes("light.living_room")
|
||||
== LightControlAttributes.BRIGHTNESS
|
||||
)
|
||||
|
||||
|
||||
async def test_schedule_profile_executes_blocks_and_restore(
|
||||
hass: HomeAssistant,
|
||||
published_automation,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue