merge with upstream/main

This commit is contained in:
Benjamin Auquite 2023-04-03 19:47:51 -05:00
commit 732c43acc1
11 changed files with 436 additions and 311 deletions

25
.github/update-services.py vendored Normal file
View file

@ -0,0 +1,25 @@
from pathlib import Path
import sys
import yaml
sys.path.append(str(Path(__file__).parent.parent))
from custom_components.adaptive_lighting import const # noqa: E402
services_filename = "custom_components/adaptive_lighting/services.yaml"
with open(services_filename) as f:
services = yaml.safe_load(f)
for service_name, dct in services.items():
_docs = {"set_manual_control": const.DOCS_MANUAL_CONTROL, "apply": const.DOCS_APPLY}
alternative_docs = _docs.get(service_name, const.DOCS)
for field_name, field in dct["fields"].items():
description = alternative_docs.get(field_name, const.DOCS[field_name])
field["description"] = description
comment = "# This file is auto-generated by .github/update-services.py."
with open(services_filename, "w") as f:
f.write(comment + "\n")
yaml.dump(services, f, sort_keys=False, width=1000, allow_unicode=True)

31
.github/update-strings.py vendored Normal file
View file

@ -0,0 +1,31 @@
import json
from pathlib import Path
import sys
sys.path.append(str(Path(__file__).parent.parent))
from custom_components.adaptive_lighting import const # noqa: E402
strings_fname = "custom_components/adaptive_lighting/strings.json"
en_fname = "custom_components/adaptive_lighting/translations/en.json"
with open(strings_fname) as f:
strings = json.load(f)
data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES}
strings["options"]["step"]["init"]["data"] = data
with open(strings_fname, "w") as f:
json.dump(strings, f, indent=2, ensure_ascii=False)
f.write("\n")
# Sync changes from strings.json to en.json
with open(en_fname) as f:
en = json.load(f)
en["config"]["step"]["user"] = strings["config"]["step"]["user"]
en["options"]["step"]["init"]["data"] = data
with open(en_fname, "w") as f:
json.dump(en, f, indent=2, ensure_ascii=False)
f.write("\n")

View file

@ -34,17 +34,23 @@ jobs:
- name: Run markdown-code-runner
run: markdown-code-runner --debug README.md
- name: Commit updated README.md
- name: Run update strings.json
run: python .github/update-strings.py
- name: Run update services.yaml
run: python .github/update-services.py
- name: Commit updated README.md, strings.json, and services.yaml
id: commit
run: |
git add README.md
git add -u .
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
if git diff --quiet && git diff --staged --quiet; then
echo "No changes in README.md, skipping commit."
echo "No changes in README.md, strings.json, and services.yaml, skipping commit."
echo "commit_status=skipped" >> $GITHUB_ENV
else
git commit -m "Update README.md"
git commit -m "Update README.md, strings.json, and services.yaml"
echo "commit_status=committed" >> $GITHUB_ENV
fi

View file

@ -88,38 +88,6 @@ The YAML and frontend configuration methods support all of the options listed be
<!-- START_OUTPUT -->
<!-- THIS CONTENT IS AUTOMATICALLY GENERATED -->
| Variable name | Description | Default | Type |
|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
| `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | `[]` | list of `entity_id`s |
| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `120` | `float` 0-6553 |
| `sleep_transition` | Duration of transition when 'sleep mode' is toggled in seconds. 😴 | `15` | `float` 0-6553 |
| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `5` | `int > 0` |
| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `6500` | `int` 1000-10000 |
| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
| `sleep_rgb_or_color_temp` | Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). 🌈 | `[255, 56, 0]` | RGB color |
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
| `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | `0` | `int` 0-10000 |
| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | `0` | `float > 0` |
| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
<!-- END_OUTPUT -->
Full example:
@ -188,7 +156,7 @@ adaptive_lighting:
|:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------|
| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s |
| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s |
| `manual_control` | Whether to add ('true') or remove ('false') the light from the 'manual_control' list. 🔒 | ❌ | bool |
| `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool |
<!-- END_OUTPUT -->
#### `adaptive_lighting.change_switch_settings`

View file

@ -22,9 +22,7 @@ CONF_NAME, DEFAULT_NAME = "name", "default"
DOCS[CONF_NAME] = "Display name for this switch. 📝"
CONF_LIGHTS, DEFAULT_LIGHTS = "lights", []
DOCS[CONF_LIGHTS] = (
"List of light entities to be controlled by Adaptive " "Lighting (may be empty). 🌟"
)
DOCS[CONF_LIGHTS] = "List of light entity_ids to be controlled (may be empty). 🌟"
CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = (
"detect_non_ha_changes",
@ -52,7 +50,7 @@ DOCS[CONF_INITIAL_TRANSITION] = (
CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 15
DOCS[CONF_SLEEP_TRANSITION] = (
"Duration of transition when 'sleep mode' is toggled " "in seconds. 😴"
'Duration of transition when "sleep mode" is toggled ' "in seconds. 😴"
)
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 5
@ -102,7 +100,7 @@ DOCS[CONF_SLEEP_COLOR_TEMP] = (
CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0]
DOCS[CONF_SLEEP_RGB_COLOR] = (
"RGB color in sleep mode (used when " "`sleep_rgb_or_color_temp` is 'rgb_color'). 🌈"
"RGB color in sleep mode (used when " '`sleep_rgb_or_color_temp` is "rgb_color"). 🌈'
)
CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = (
@ -110,7 +108,7 @@ CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = (
"color_temp",
)
DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = (
"Use either `'rgb_color'` or `'color_temp'` " "in sleep mode. 🌙"
'Use either `"rgb_color"` or `"color_temp"` ' "in sleep mode. 🌙"
)
CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0
@ -163,13 +161,13 @@ DOCS[CONF_ADAPT_UNTIL_SLEEP] = (
CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0
DOCS[CONF_ADAPT_DELAY] = (
"Wait time (seconds) between light turn on and Adaptive Lighting applying "
"changes. Helps avoid flickering. ⏲️"
"changes. Might help to avoid flickering. ⏲️"
)
CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0
DOCS[CONF_SEND_SPLIT_DELAY] = (
"Wait time (milliseconds) between commands when using `separate_turn_on_commands`. "
"Helps ensure correct handling. ⏲️"
"Delay (ms) between `separate_turn_on_commands` for lights that don't support "
"simultaneous brightness and color setting. ⏲️"
)
CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL = "autoreset_control_seconds", 0
@ -197,7 +195,11 @@ CONF_TURN_ON_LIGHTS = "turn_on_lights"
DOCS[CONF_TURN_ON_LIGHTS] = "Whether to turn on lights that are currently off. 🔆"
SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings"
CONF_USE_DEFAULTS = "use_defaults"
DOCS[CONF_USE_DEFAULTS] = "Whether to use default settings for the switches. ⚙️"
DOCS[CONF_USE_DEFAULTS] = (
"Sets the default values not specified in this service call. Options: "
'"current" (default, retains current values), "factory" (resets to '
'documented defaults), or "configuration" (reverts to switch config defaults). ⚙️'
)
TURNING_OFF_DELAY = 5
@ -206,8 +208,8 @@ DOCS_MANUAL_CONTROL = {
"light as being `manually controlled`. 📝",
CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the "
"switch are selected. 💡",
CONF_MANUAL_CONTROL: "Whether to add ('true') or remove ('false') the "
"light from the 'manual_control' list. 🔒",
CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") the '
'light from the "manual_control" list. 🔒',
}
DOCS_APPLY = {

View file

@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": [],
"version": "1.9.1"
"version": "1.10.0"
}

View file

@ -1,187 +1,186 @@
# This file is auto-generated by .github/update-services.py.
apply:
description: Applies the current Adaptive Lighting settings to lights.
fields:
entity_id:
description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used.
description: The `entity_id` of the switch with the settings to apply. 📝
selector:
entity:
integration: adaptive_lighting
domain: switch
multiple: false
lights:
description: entity_id(s) of lights, if not specified, all lights in the switch are selected.
description: A light (or list of lights) to apply the settings to. 💡
selector:
entity:
domain: light
multiple: true
transition:
description: Transition of the lights.
description: Duration of transition when lights change, in seconds. 🕑
example: 10
selector:
text:
text: null
adapt_brightness:
description: "Adapt the 'brightness', default: true"
description: Whether to adapt the brightness of the light. 🌞
example: true
selector:
boolean:
boolean: null
adapt_color:
description: "Adapt the color_temp/color_rgb, default: true"
description: Whether to adapt the color on supporting lights. 🌈
example: true
selector:
boolean:
boolean: null
prefer_rgb_color:
description: "Prefer to use color_rgb over color_temp if possible, default: false"
description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈
example: false
selector:
boolean:
boolean: null
turn_on_lights:
description: "Turn on the lights that are off, default: false"
description: Whether to turn on lights that are currently off. 🔆
example: false
selector:
boolean:
boolean: null
set_manual_control:
description: Mark whether a light is 'manually controlled'.
fields:
entity_id:
description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used.
description: The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝
selector:
entity:
integration: adaptive_lighting
domain: switch
multiple: false
lights:
description: entity_id(s) of lights, if not specified, all lights in the switch are selected.
description: entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡
selector:
entity:
domain: light
multiple: true
manual_control:
description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true"
description: Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒
example: true
default: true
selector:
boolean:
boolean: null
change_switch_settings:
description: "Change any settings you'd like in the switch. All options here are the same as in the config flow."
description: Change any settings you'd like in the switch. All options here are the same as in the config flow.
fields:
entity_id:
description: "entity_id of the Adaptive Lighting switch."
description: Entity ID of the switch. 📝
required: true
selector:
entity:
domain: switch
use_defaults:
description: "(default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation."
example: "current"
description: 'Sets the default values not specified in this service call. Options: "current" (default, retains current values), "factory" (resets to documented defaults), or "configuration" (reverts to switch config defaults). ⚙️'
example: current
required: false
default: "current"
default: current
selector:
select:
options:
- "current"
- "configuration"
- "factory"
- current
- configuration
- factory
include_config_in_attributes:
description: "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)"
description: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝
required: false
selector:
boolean:
boolean: null
turn_on_lights:
description: "Turn on the lights that are off, default: false"
description: Whether to turn on lights that are currently off. 🔆
example: false
required: false
selector:
boolean:
boolean: null
initial_transition:
description: "initial_transition: When lights turn 'off' to 'on'. (seconds)"
description: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️
example: 1
required: false
selector:
text:
text: null
sleep_transition:
description: "sleep_transition: When 'sleep_state' changes. (seconds)"
description: Duration of transition when "sleep mode" is toggled in seconds. 😴
example: 1
required: false
selector:
text:
text: null
max_brightness:
description: "max_brightness: Highest brightness of lights during a cycle. (%)"
description: Maximum brightness percentage. 💡
required: false
example: 100
selector:
text:
text: null
max_color_temp:
description: "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)"
description: Coldest color temperature in Kelvin. ❄️
required: false
example: 5500
selector:
text:
text: null
min_brightness:
description: "min_brightness: Lowest brightness of lights during a cycle. (%)"
description: Minimum brightness percentage. 💡
required: false
example: 1
selector:
text:
text: null
min_color_temp:
description: "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)"
description: Warmest color temperature in Kelvin. 🔥
required: false
example: 2000
selector:
text:
text: null
only_once:
description: "only_once: Only adapt the lights when turning them on."
description: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄
example: false
required: false
selector:
boolean:
boolean: null
prefer_rgb_color:
description: "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible."
description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈
required: false
example: false
selector:
boolean:
boolean: null
separate_turn_on_commands:
description: "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights)."
description: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀
required: false
example: false
selector:
boolean:
boolean: null
send_split_delay:
description: "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly."
description: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️
required: false
example: 0
selector:
boolean:
boolean: null
sleep_brightness:
description: "sleep_brightness, Brightness setting for Sleep Mode. (%)"
description: Brightness percentage of lights in sleep mode. 😴
required: false
example: 1
selector:
text:
text: null
sleep_rgb_or_color_temp:
description: "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'"
description: Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙
required: false
example: "color_temp"
example: color_temp
selector:
select:
options:
- "rgb_color"
- "color_temp"
- rgb_color
- color_temp
sleep_rgb_color:
description: "sleep_rgb_color, in RGB"
description: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈
required: false
selector:
color_rgb:
color_rgb: null
sleep_color_temp:
description: "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)"
description: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴
required: false
example: 1000
selector:
text:
text: null
sunrise_offset:
description: sunrise_offset, in +/- seconds (integer)
description: Adjust sunrise time with a positive or negative offset in seconds. ⏰
required: false
example: 0
selector:
@ -189,64 +188,64 @@ change_switch_settings:
min: 0
max: 86300
sunrise_time:
description: sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)
description: Set a fixed time (HH:MM:SS) for sunrise. 🌅
required: false
example: ""
example: ''
selector:
time:
time: null
sunset_offset:
description: sunset_offset, in +/- seconds (integer)
description: Adjust sunset time with a positive or negative offset in seconds. ⏰
required: false
example: ""
example: ''
selector:
number:
min: 0
max: 86300
sunset_time:
description: sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)
example: ""
description: Set a fixed time (HH:MM:SS) for sunset. 🌇
example: ''
required: false
selector:
time:
time: null
max_sunrise_time:
description: "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)"
example: ""
description: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅
example: ''
required: false
selector:
time:
time: null
min_sunset_time:
description: "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)"
example: ""
description: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇
example: ''
required: false
selector:
time:
time: null
take_over_control:
description: "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on."
description: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒
required: false
example: true
selector:
boolean:
boolean: null
detect_non_ha_changes:
description: "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)"
description: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️
required: false
example: false
selector:
boolean:
boolean: null
transition:
description: "Transition time when applying a change to the lights (seconds)"
description: Duration of transition when lights change, in seconds. 🕑
required: false
example: 45
selector:
text:
text: null
adapt_delay:
description: "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering."
description: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️
required: false
example: 0
selector:
text:
text: null
autoreset_control_seconds:
description: "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)"
description: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️
required: false
example: 0
selector:
text:
text: null

View file

@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"title": "Choose a name for the Adaptive Lighting",
"title": "Choose a name for the Adaptive Lighting instance",
"description": "Every instance can contain multiple lights!",
"data": {
"name": "Name"
@ -19,35 +19,35 @@
"title": "Adaptive Lighting options",
"description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.",
"data": {
"lights": "lights",
"initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)",
"include_config_in_attributes": "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)",
"adapt_until_sleep": "adapt_until_sleep: When checked, Adaptive Lighting will use the sleep settings as the minimum, and transition to these values past the sunset (default: false)",
"sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)",
"interval": "interval: Time between switch updates. (seconds)",
"max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)",
"max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)",
"min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)",
"min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)",
"only_once": "only_once: Only adapt the lights when turning them on.",
"prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.",
"separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).",
"send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.",
"sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)",
"sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'",
"sleep_rgb_color": "sleep_rgb_color, in RGB",
"sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)",
"sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)",
"sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
"max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
"sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)",
"sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
"min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
"take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.",
"detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)",
"transition": "Transition time when applying a change to the lights (seconds)",
"adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering.",
"autoreset_control_seconds": "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)"
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
"prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
"initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
"sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
"transition": "transition: Duration of transition when lights change, in seconds. 🕑",
"transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
"interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
"min_brightness": "min_brightness: Minimum brightness percentage. 💡",
"max_brightness": "max_brightness: Maximum brightness percentage. 💡",
"min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥",
"max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️",
"sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴",
"sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
"sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
"sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
"sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
"max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
"sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
"sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
"min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️"
}
}
},

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import base64
import bisect
from collections import defaultdict
from collections.abc import Callable, Coroutine
from copy import deepcopy
from dataclasses import dataclass
import datetime
@ -802,10 +802,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._only_once = data[CONF_ONLY_ONCE]
self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR]
self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS]
self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
self._transition = data[CONF_TRANSITION]
self._adapt_delay = data[CONF_ADAPT_DELAY]
self._send_split_delay = data[CONF_SEND_SPLIT_DELAY]
self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
if not data[CONF_TAKE_OVER_CONTROL] and data[CONF_DETECT_NON_HA_CHANGES]:
_LOGGER.warning(
"%s: Config mismatch: 'detect_non_ha_changes: true' "
"requires 'take_over_control' to be enabled. Adjusting config "
"and continuing setup with `take_over_control: true`.",
self._name,
)
self._take_over_control = True
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
self._expand_light_groups() # updates manual control timers
_loc = get_astral_location(self.hass)
@ -1128,11 +1137,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
)
self.async_write_ha_state()
if lights is None:
lights = self._lights
if (self._only_once and not force) or not lights:
if not force and self._only_once:
return
await self._adapt_lights(lights, transition, force, context)
filtered_lights = []
for light in lights:
# Don't adapt lights that haven't finished prior transitions.
if force or not self.turn_on_off_listener.transition_timers.get(light):
filtered_lights.append(light)
if not filtered_lights:
return
await self._adapt_lights(filtered_lights, transition, force, context)
async def _adapt_lights(
self,
@ -1532,8 +1553,6 @@ class TurnOnOffListener:
self.sleep_tasks: dict[str, asyncio.Task] = {}
# Tracks which lights are manually controlled
self.manual_control: dict[str, bool] = {}
# Counts the number of times (in a row) a light had a changed state.
self.cnt_significant_changes: dict[str, int] = defaultdict(int)
# Track 'state_changed' events of self.lights resulting from this integration
self.last_state_change: dict[str, list[State]] = {}
# Track last 'service_data' to 'light.turn_on' resulting from this integration
@ -1543,9 +1562,8 @@ class TurnOnOffListener:
self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {}
self.auto_reset_manual_control_times: dict[str, float] = {}
# When a state is different `max_cnt_significant_changes` times in a row,
# mark it as manually_controlled.
self.max_cnt_significant_changes = 2
# Track light transitions
self.transition_timers: dict[str, _AsyncSingleShotTimer] = {}
self.remove_listener = self.hass.bus.async_listen(
EVENT_CALL_SERVICE, self.turn_on_off_event_listener
@ -1554,6 +1572,56 @@ class TurnOnOffListener:
EVENT_STATE_CHANGED, self.state_changed_event_listener
)
def _handle_timer(
self,
light: str,
timers_dict: dict[str, _AsyncSingleShotTimer],
delay: float | None,
reset_coroutine: Callable[[], Coroutine[Any, Any, None]],
) -> None:
timer = timers_dict.get(light)
if timer is not None:
if delay is None: # Timer object exists, but should not anymore
timer.cancel()
timers_dict.pop(light)
else: # Timer object already exists, just update the delay and restart it
timer.delay = delay
timer.start()
elif delay is not None: # Timer object does not exist, create it
timer = _AsyncSingleShotTimer(delay, reset_coroutine)
timers_dict[light] = timer
timer.start()
def start_transition_timer(self, light: str) -> None:
"""Mark a light as manually controlled."""
_LOGGER.debug("Start transition timer for %s", light)
last_service_data = self.last_service_data
if (
not last_service_data
or light not in last_service_data
or ATTR_TRANSITION not in last_service_data[light]
):
return
delay = last_service_data[light][ATTR_TRANSITION]
async def reset():
_LOGGER.debug(
"Transition finished for light %s",
light,
)
switches = _get_switches_with_lights(self.hass, [light])
for switch in switches:
if not switch.is_on:
continue
await switch._update_attrs_and_maybe_adapt_lights(
[light],
force=False,
context=switch.create_context("transit"),
)
self._handle_timer(light, self.transition_timers, delay, reset)
def set_auto_reset_manual_control_times(self, lights: list[str], time: float):
"""Set the time after which the lights are automatically reset."""
if time == 0:
@ -1576,40 +1644,28 @@ class TurnOnOffListener:
_LOGGER.debug("Marking '%s' as manually controlled.", light)
self.manual_control[light] = True
delay = self.auto_reset_manual_control_times.get(light)
timer = self.auto_reset_manual_control_timers.get(light)
if timer is not None:
if delay is None: # Timer object exists, but should not anymore
timer.cancel()
self.auto_reset_manual_control_timers.pop(light)
else: # Timer object already exists, just update the delay and restart it
timer.delay = delay
timer.start()
elif delay is not None: # Timer object does not exist, create it
async def reset():
self.reset(light)
switches = _get_switches_with_lights(self.hass, [light])
for switch in switches:
if not switch.is_on:
continue
# pylint: disable=protected-access
await switch._update_attrs_and_maybe_adapt_lights(
[light],
transition=switch._initial_transition,
force=True,
context=switch.create_context("autoreset"),
)
_LOGGER.debug(
"Auto resetting 'manual_control' status of '%s' because"
" it was not manually controlled for %s seconds.",
light,
delay,
async def reset():
self.reset(light)
switches = _get_switches_with_lights(self.hass, [light])
for switch in switches:
if not switch.is_on:
continue
await switch._update_attrs_and_maybe_adapt_lights(
[light],
transition=switch._initial_transition,
force=True,
context=switch.create_context("autoreset"),
)
assert not self.manual_control[light]
_LOGGER.debug(
"Auto resetting 'manual_control' status of '%s' because"
" it was not manually controlled for %s seconds.",
light,
delay,
)
assert not self.manual_control[light]
timer = _AsyncSingleShotTimer(delay, reset)
self.auto_reset_manual_control_timers[light] = timer
timer.start()
self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset)
def reset(self, *lights, reset_manual_control=True) -> None:
"""Reset the 'manual_control' status of the lights."""
@ -1621,7 +1677,6 @@ class TurnOnOffListener:
timer.cancel()
self.last_state_change.pop(light, None)
self.last_service_data.pop(light, None)
self.cnt_significant_changes[light] = 0
async def turn_on_off_event_listener(self, event: Event) -> None:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
@ -1700,11 +1755,7 @@ class TurnOnOffListener:
new_state.context.id,
)
if (
new_state is not None
and new_state.state == STATE_ON
and is_our_context(new_state.context)
):
if new_state is not None and new_state.state == STATE_ON:
# It is possible to have multiple state change events with the same context.
# This can happen because a `turn_on.light(brightness_pct=100, transition=30)`
# event leads to an instant state change of
@ -1717,21 +1768,29 @@ class TurnOnOffListener:
# incorrect 'min_kelvin' and 'max_kelvin', which happens e.g., for
# Philips Hue White GU10 Bluetooth lights).
old_state: list[State] | None = self.last_state_change.get(entity_id)
if (
old_state is not None
and old_state[0].context.id == new_state.context.id
):
# If there is already a state change event from this event (with this
# context) then append it to the already existing list.
_LOGGER.debug(
"State change event of '%s' is already in 'self.last_state_change' (%s)"
" adding this state also",
entity_id,
new_state.context.id,
)
if is_our_context(new_state.context):
if (
old_state is not None
and old_state[0].context.id == new_state.context.id
):
_LOGGER.debug(
"TurnOnOffListener: State change event of '%s' is already"
" in 'self.last_state_change' (%s)"
" adding this state also",
entity_id,
new_state.context.id,
)
self.last_state_change[entity_id].append(new_state)
else:
_LOGGER.debug(
"TurnOnOffListener: New adapt '%s' found for %s",
new_state,
entity_id,
)
self.last_state_change[entity_id] = [new_state]
self.start_transition_timer(entity_id)
elif old_state is not None:
self.last_state_change[entity_id].append(new_state)
else:
self.last_state_change[entity_id] = [new_state]
def is_manually_controlled(
self,
@ -1786,64 +1845,58 @@ class TurnOnOffListener:
detected, we mark the light as 'manually controlled' until the light
or switch is turned 'off' and 'on' again.
"""
if light not in self.last_state_change:
return False
old_states: list[State] = self.last_state_change[light]
await self.hass.helpers.entity_component.async_update_entity(light)
new_state = self.hass.states.get(light)
last_service_data = self.last_service_data.get(light)
if last_service_data is None:
return
compare_to = functools.partial(
_attributes_have_changed,
light=light,
new_attributes=new_state.attributes,
adapt_brightness=adapt_brightness,
adapt_color=adapt_color,
context=context,
)
for index, old_state in enumerate(old_states):
changed = compare_to(old_attributes=old_state.attributes)
if not changed:
_LOGGER.debug(
"State of '%s' didn't change wrt change event nr. %s (context.id=%s)",
light,
index,
context.id,
)
break
last_service_data = self.last_service_data.get(light)
if changed and last_service_data is not None:
# It can happen that the state change events that are associated
# with the last 'light.turn_on' call by this integration were not
# final states. Possibly a later EVENT_STATE_CHANGED happened, where
# the correct target brightness/color was reached.
changed = compare_to(old_attributes=last_service_data)
if not changed:
# Update state and check for a manual change not done in HA.
# Ensure HASS is correctly updating your light's state with
# light.turn_on calls if any problems arise. This
# can happen e.g. using zigbee2mqtt with 'report: false' in device settings.
if switch._detect_non_ha_changes:
_LOGGER.debug(
"%s: 'detect_non_ha_changes: true', calling update_entity(%s)"
" and check if it's last adapt succeeded.",
switch._name,
light,
)
# This update_entity probably isn't necessary now that we're checking
# if transitions finished from our last adapt.
await self.hass.helpers.entity_component.async_update_entity(light)
refreshed_state = self.hass.states.get(light)
_LOGGER.debug(
"%s: Current state of %s: %s",
switch._name,
light,
refreshed_state,
)
changed = compare_to(
old_attributes=last_service_data,
new_attributes=refreshed_state.attributes,
)
if changed:
_LOGGER.debug(
"State of '%s' didn't change wrt 'last_service_data' (context.id=%s)",
light,
context.id,
)
n_changes = self.cnt_significant_changes[light]
if changed:
self.cnt_significant_changes[light] += 1
if n_changes >= self.max_cnt_significant_changes:
# Only mark a light as significantly changing, if changed==True
# N times in a row. We do this because sometimes a state changes
# happens only *after* a new update interval has already started.
self.mark_as_manual_control(light)
_fire_manual_control_event(switch, light, context, is_async=False)
else:
if n_changes > 1:
_LOGGER.debug(
"State of '%s' had 'cnt_significant_changes=%s' but the state"
" changed to the expected settings now",
light,
n_changes,
)
self.cnt_significant_changes[light] = 0
return changed
return True
_LOGGER.debug(
"%s: Light '%s' correctly matches our last adapt's service data, continuing..."
" context.id=%s.",
switch._name,
light,
context.id,
)
return False
async def maybe_cancel_adjusting(
self, entity_id: str, off_to_on_event: Event, on_to_off_event: Event | None

View file

@ -4,7 +4,7 @@
"step": {
"user": {
"title": "Choose a name for the Adaptive Lighting instance",
"description": "Pick a name for this instance. You can run several instances of Adaptive lighting, each of these can contain multiple lights!",
"description": "Every instance can contain multiple lights!",
"data": {
"name": "Name"
}
@ -20,33 +20,35 @@
"title": "Adaptive Lighting options",
"description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.",
"data": {
"lights": "lights",
"initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)",
"include_config_in_attributes": "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)",
"sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)",
"interval": "interval: Time between switch updates. (seconds)",
"max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)",
"max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)",
"min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)",
"min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)",
"only_once": "only_once: Only adapt the lights when turning them on.",
"prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.",
"separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).",
"send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.",
"sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)",
"sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'",
"sleep_rgb_color": "sleep_rgb_color, in RGB",
"sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)",
"sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)",
"sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
"max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
"sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)",
"sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
"min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
"take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.",
"detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)",
"transition": "Transition time when applying a change to the lights (seconds)",
"adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering."
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
"prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
"initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
"sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
"transition": "transition: Duration of transition when lights change, in seconds. 🕑",
"transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
"interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
"min_brightness": "min_brightness: Minimum brightness percentage. 💡",
"max_brightness": "max_brightness: Maximum brightness percentage. 💡",
"min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥",
"max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️",
"sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴",
"sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
"sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
"sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
"sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
"max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
"sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
"sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
"min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️"
}
}
},

View file

@ -56,6 +56,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_AREA_ID,
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
CONF_LIGHTS,
CONF_NAME,
CONF_PLATFORM,
@ -769,25 +770,63 @@ async def test_significant_change(hass):
)
await hass.async_block_till_done()
switch, (bed_light_instance, *_) = await setup_lights_and_switch(hass)
async def set_brightness(val: int):
hass.states.async_set(
ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1}
)
await hass.async_block_till_done()
switch, _ = await setup_lights_and_switch(hass)
_LOGGER.debug("Test detect_non_ha_changes:")
switch._take_over_control = True
assert switch._take_over_control
switch._detect_non_ha_changes = True
assert switch._detect_non_ha_changes
# build last service data
await update(force=False)
# force=True should not reset manual control.
await turn_light(True, brightness=40)
await turn_light(True, brightness=20)
await update(force=False)
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
await update(force=True)
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
# turn light off then on should reset manual control.
await turn_light(False)
await turn_light(True)
await update(force=True) # removes manual control
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
# Change brightness by setting state (not using 'light.turn_on')
attributes = hass.states.get(ENTITY_LIGHT).attributes
new_attributes = attributes.copy()
new_brightness = (attributes[ATTR_BRIGHTNESS] + 100) % 255
new_attributes[ATTR_BRIGHTNESS] = new_brightness
bed_light_instance._brightness = new_brightness
# Assert last_service_data got filled from update()
await update(force=True)
assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
for _ in range(switch.turn_on_off_listener.max_cnt_significant_changes):
# Simulate a transition to 255 where the update() is already using brightness 255.
await set_brightness(240)
await set_brightness(244)
await set_brightness(247)
await set_brightness(250)
# last_state_change should have our state changes.
# Change brightness by async_set (not using 'light.turn_on')
new_brightness = 50
await set_brightness(new_brightness)
_LOGGER.debug("Test: Brightness set to %s", new_brightness)
# mock homeassistant.core.HomeAssistant.helpers.entity_component.async_update_entity
# Otherwise what happens is update_entity() refreshes the state to the last call of
# light.turn_on(). This is because we are not using hass.states.async_set() to
# set the brightness of the light. We mock `async_update_ha_state` because
# `async_update_entity` calls it.
with patch("homeassistant.helpers.entity.Entity.async_update_ha_state"):
# On next update ENTITY_LIGHT should be marked as manually controlled
await update(force=False)
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
# On next update the light should be marked as manually controlled
await update(force=False)
# TODO: the state should be `bool(manual_control) is True`
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
assert (
switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
)
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
def test_color_difference_redmean():