Compare commits

...

12 commits

Author SHA1 Message Date
pre-commit-ci[bot]
fdedba9b40 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2023-04-29 22:08:18 +00:00
Benjamin Auquite
1a5cf9d410
Merge branch 'main' into adaptive-lighting-edge 2023-04-29 17:08:11 -05:00
Benjamin Auquite
9a2943db50 Revert a888afd6dc 2023-04-11 19:45:40 -05:00
Benjamin Auquite
b87ee215b3 didn't mean to include this 2023-04-11 19:37:51 -05:00
Benjamin Auquite
77812de08c remove unconventional sun events check
never finished
2023-04-11 19:30:26 -05:00
Benjamin Auquite
ea2e533186 squash 'flexible service calls' 2023-04-11 19:16:56 -05:00
Benjamin Auquite
9bec45b63c Squashed commit of the following:
commit 1edaf1d7ed
Merge: 0e7e06e a888afd
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 12:57:30 2023 -0500

    Merge branch 'main' into unconventional_sunrise_sunset_times

commit 0e7e06e8a4
Merge: 8fa554a fe7bdd3
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Sat Apr 8 20:23:20 2023 -0500

    Merge branch 'main' into unconventional_sunrise_sunset_times

commit 8fa554a4e7
Merge: ec64eda 39e9d0e
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Sat Apr 8 00:50:16 2023 -0500

    merge

commit ec64eda6bf
Merge: 8353db5 744e43f
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Tue Apr 4 21:08:54 2023 -0500

    Merge branch 'main' into unconventional_sunrise_sunset_times

commit 8353db54cc
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Tue Apr 4 00:56:24 2023 -0500

    tests are unfinished

commit 6cb24116c0
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 3 21:17:01 2023 -0500

    Update switch.py
2023-04-11 19:12:20 -05:00
Benjamin Auquite
ce4e04358e Squashed commit of the following:
commit 1ddae7ac48
Merge: 0a4c7ed a888afd
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 12:52:56 2023 -0500

    Merge branch 'main' into compare-service-datas-with-ignore-fields

commit 0a4c7ed1dd
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Sun Apr 9 23:26:41 2023 -0500

    Revert "keep line length under 100"

    This reverts commit 461efb5fed.

commit 461efb5fed
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Sun Apr 9 22:38:04 2023 -0500

    keep line length under 100

commit beece5c06f
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Sun Apr 9 22:33:50 2023 -0500

    ignore ATTR_TRANSITION in checks

# Conflicts:
#	README.md
2023-04-11 19:11:28 -05:00
Benjamin Auquite
f0b487320d Squashed commit of the following:
commit 5b927dbb02
Merge: 78395ee a888afd
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 12:52:46 2023 -0500

    Merge branch 'main' into adjust_adapt_until_sleep

commit 78395eea65
Author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Date:   Mon Apr 10 09:38:11 2023 +0000

    Update README.md, strings.json, and services.yaml

commit bc39f5e977
Merge: e30b7de c4b5067
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 09:36:39 2023 +0000

    Merge c4b5067b05 into e30b7debe5

commit c4b5067b05
Merge: 7244a5f 91d6065
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:36:25 2023 -0500

    Merge branch 'adjust_adapt_until_sleep' of https://github.com/basnijholt/adaptive-lighting into adjust_adapt_until_sleep

commit 7244a5f483
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:36:14 2023 -0500

    Update const.py

commit 91d606586f
Author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Date:   Mon Apr 10 09:28:58 2023 +0000

    Update README.md, strings.json, and services.yaml

commit 4509c0a0f1
Merge: e30b7de e46d867
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 09:27:02 2023 +0000

    Merge e46d867854 into e30b7debe5

commit e46d867854
Merge: e7e266b 3b61f49
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:26:56 2023 -0500

    Merge branch 'adjust_adapt_until_sleep' of https://github.com/basnijholt/adaptive-lighting into adjust_adapt_until_sleep

commit e7e266b5a1
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:26:47 2023 -0500

    Update const.py

commit 3b61f49fb3
Author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Date:   Mon Apr 10 09:26:09 2023 +0000

    Update README.md, strings.json, and services.yaml

commit 7f952e0658
Merge: e30b7de a4d5f86
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 09:24:27 2023 +0000

    Merge a4d5f861ff into e30b7debe5

commit a4d5f861ff
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:24:20 2023 -0500

    autofill workaround

commit 06dd2a5c9a
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:23:01 2023 -0500

    Revert "Merge branch 'adjust_adapt_until_sleep' of https://github.com/basnijholt/adaptive-lighting into adjust_adapt_until_sleep"

    This reverts commit bca9f1bd3d, reversing
    changes made to 840aab6158.

commit bca9f1bd3d
Merge: 840aab6 2d74a31
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:22:19 2023 -0500

    Merge branch 'adjust_adapt_until_sleep' of https://github.com/basnijholt/adaptive-lighting into adjust_adapt_until_sleep

commit 840aab6158
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:22:07 2023 -0500

    add `adapt_color_temp_until_sleep`

    supports backward-compatibility

commit 2d74a313bb
Author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Date:   Mon Apr 10 09:14:31 2023 +0000

    Update README.md, strings.json, and services.yaml

commit 553265a1e8
Merge: e30b7de b53a5af
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 09:12:53 2023 +0000

    Merge b53a5aff68 into e30b7debe5

commit b53a5aff68
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:11:48 2023 -0500

    add new config option

commit 4e1c769feb
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 04:06:09 2023 -0500

    initial commit
2023-04-11 19:10:37 -05:00
Benjamin Auquite
d1a7b9f970 Squashed commit of the following:
commit aad2eaf595
Merge: 090c0ab a888afd
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 12:52:37 2023 -0500

    Merge branch 'main' into add_flat_limits_to_configuration

commit 090c0abc6d
Author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Date:   Mon Apr 10 09:30:40 2023 +0000

    Update README.md, strings.json, and services.yaml

commit cb27264d6a
Merge: e30b7de f18c5f1
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 09:28:50 2023 +0000

    Merge f18c5f161b into e30b7debe5

commit f18c5f161b
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 03:59:33 2023 -0500

    initial commit
2023-04-11 19:09:38 -05:00
Benjamin Auquite
7895075e76 Squashed commit of the following:
commit 7069d693b8
Merge: adde5b5 a888afd
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 12:52:19 2023 -0500

    Merge branch 'main' into add-watched-lights

commit adde5b5645
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 10:34:24 2023 -0500

    fix pytest

commit 2c5eaec2d3
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 07:27:25 2023 -0500

    Revert "Update const.py"

    This reverts commit e773af07ca.

commit e773af07ca
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 07:20:23 2023 -0500

    Update const.py

commit c167f59ccd
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 06:09:14 2023 -0500

    Update docs

commit 6220df57d1
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:59:26 2023 -0500

    fix the test

commit ff292c2c1b
Merge: 4fdf42b 19d4468
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:39:17 2023 -0500

    Merge branch 'add-watched-lights' of https://github.com/basnijholt/adaptive-lighting into add-watched-lights

commit 4fdf42bea1
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:39:05 2023 -0500

    small fixes

commit 19d4468f63
Author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Date:   Mon Apr 10 10:36:54 2023 +0000

    Update README.md, strings.json, and services.yaml

commit 70c90181f0
Merge: e30b7de b6a9265
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 10:34:49 2023 +0000

    Merge b6a92650a9 into e30b7debe5

commit b6a92650a9
Merge: 784eed0 ae04ab4
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:34:43 2023 -0500

    Merge branch 'add-watched-lights' of https://github.com/basnijholt/adaptive-lighting into add-watched-lights

commit 784eed09b3
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:34:33 2023 -0500

    squash merge `alt_detect_method` again

commit ae04ab49a8
Author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Date:   Mon Apr 10 10:34:12 2023 +0000

    Update README.md, strings.json, and services.yaml

commit cfbd65f976
Merge: e30b7de 3c5ea73
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 10:32:34 2023 +0000

    Merge 3c5ea73341 into e30b7debe5

commit 3c5ea73341
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:32:27 2023 -0500

    Update switch.py

commit 2f750694d9
Merge: 429251b 7a9dc8f
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:31:13 2023 -0500

    Merge branch 'add-watched-lights' of https://github.com/basnijholt/adaptive-lighting into add-watched-lights

commit 429251b45f
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:31:04 2023 -0500

    update docs

commit 7a9dc8f24a
Author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Date:   Mon Apr 10 10:29:37 2023 +0000

    Update README.md, strings.json, and services.yaml

commit 37de4244b1
Merge: e30b7de d52ea53
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 10:28:01 2023 +0000

    Merge d52ea53dcc into e30b7debe5

commit d52ea53dcc
Merge: dbc47c8 e30b7de
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:27:43 2023 -0500

    Merge branch 'main' into add-watched-lights

commit dbc47c8a36
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:26:11 2023 -0500

    Update switch.py

commit 2db675a434
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:25:44 2023 -0500

    merge with `alt_detect_method`

    required for this PR

commit dced97b5ca
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 05:21:51 2023 -0500

    merge with `origin\main`
2023-04-11 19:08:49 -05:00
Benjamin Auquite
b6790b14eb Squashed commit of the following:
commit 8dd1b5660e
Merge: 3b3f56f a888afd
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 12:51:11 2023 -0500

    merge origin

commit 3b3f56f2d2
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 12:39:26 2023 -0500

    Update test_switch.py

commit 258964d97b
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Mon Apr 10 12:07:18 2023 -0500

    pass flake8

    move stuff out of `async_setup_entry`

commit 5b6ad02e1d
Merge: 251babc fe7bdd3
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Sat Apr 8 06:01:44 2023 -0500

    Merge branch 'main' into new-service-calls

commit 251babce9d
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Sat Apr 8 05:35:16 2023 -0500

    fixes and now passes tests

commit d9cd74bef3
Author: Benjamin Auquite <halomastar@gmail.com>
Date:   Sat Apr 8 05:15:17 2023 -0500

    initial commit, passes most tests.

    'function too complex'
2023-04-11 19:07:48 -05:00
7 changed files with 766 additions and 333 deletions

View file

@ -34,7 +34,7 @@ runs:
- name: Install dependencies - name: Install dependencies
shell: bash shell: bash
run: | run: |
echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###" echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in '\workflows\install_dependencies\action.yml'. ###"
pip install -r core/requirements.txt --use-pep517 pip install -r core/requirements.txt --use-pep517
# because they decided to pull codecov the package from PyPI... # because they decided to pull codecov the package from PyPI...
sed -i '/codecov/d' core/requirements_test.txt sed -i '/codecov/d' core/requirements_test.txt

View file

@ -90,37 +90,39 @@ The YAML and frontend configuration methods support all of the options listed be
<!-- START_OUTPUT --> <!-- START_OUTPUT -->
<!-- THIS CONTENT IS AUTOMATICALLY GENERATED --> <!-- THIS CONTENT IS AUTOMATICALLY GENERATED -->
| Variable name | Description | Default | Type | | Variable name | Description | Default | Type |
|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------| |:-------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | | `lights` | List of light entity_ids to be controlled (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` | | `watched_lights` | Use this dictionary of lights to check for manually controlled events in addition to the main lights 🌟 Example: {light.watch_light: light.main_light} will fire manually controlled events to light.main_light | `{}` | list of `entity_id`s |
| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` | | `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | | `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | | `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 | | `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `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` | | `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` | | `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 | | `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 | | `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 | | `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | | `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | | `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` | | `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
| `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_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | 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 |
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` | | `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` | | `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | | `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` | | `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` | | `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | | `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` | | `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
| `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` | | `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `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` |
| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` | | `alt_detect_method` | alt_detect_method: When true, will check for any significant changes in the opposite direction of where adaptive-lighting tried to adapt last. This is an alternative to 'detect_non_ha_changes' (default: false) | `False` | `bool` |
| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | | `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | | `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | | `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 |
| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to 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 --> <!-- END_OUTPUT -->

View file

@ -1,7 +1,12 @@
"""Constants for the Adaptive Lighting integration.""" """Constants for the Adaptive Lighting integration."""
from homeassistant.components.light import VALID_TRANSITION from homeassistant.components.light import VALID_TRANSITION
from homeassistant.const import CONF_ENTITY_ID from homeassistant.const import (
CONF_ENTITY_ID,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
)
from homeassistant.helpers import selector from homeassistant.helpers import selector
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import voluptuous as vol import voluptuous as vol
@ -33,6 +38,12 @@ DOCS[CONF_DETECT_NON_HA_CHANGES] = (
"Requires `take_over_control`. 🕵️" "Requires `take_over_control`. 🕵️"
) )
CONF_ALT_DETECT_METHOD, DEFAULT_ALT_DETECT_METHOD = "alt_detect_method", False
DOCS[CONF_ALT_DETECT_METHOD] = (
"alt_detect_method: When true, will check for any significant changes in the opposite direction"
" of where adaptive-lighting tried to adapt last."
" This is an alternative to 'detect_non_ha_changes' (default: false)"
)
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = ( CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = (
"include_config_in_attributes", "include_config_in_attributes",
False, False,
@ -68,6 +79,14 @@ DOCS[CONF_MIN_BRIGHTNESS] = "Minimum brightness percentage. 💡"
CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000 CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP = "min_color_temp", 2000
DOCS[CONF_MIN_COLOR_TEMP] = "Warmest color temperature in Kelvin. 🔥" DOCS[CONF_MIN_COLOR_TEMP] = "Warmest color temperature in Kelvin. 🔥"
CONF_FLAT_LIMITS, DEFAULT_FLAT_LIMITS = "flat_limits", False
DOCS[CONF_FLAT_LIMITS] = (
"When True, will not calculate between the"
" max/min supported limits of your light. Example: when adapting brightness to 50% while "
+ CONF_MAX_BRIGHTNESS
+ " is set to 80%, Adaptive Lighting will use 80% instead of 90%"
)
CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE = "only_once", False
DOCS[CONF_ONLY_ONCE] = ( DOCS[CONF_ONLY_ONCE] = (
"Adapt lights only when they are turned on (`true`) or keep adapting them " "Adapt lights only when they are turned on (`true`) or keep adapting them "
@ -154,9 +173,24 @@ CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP = (
False, False,
) )
DOCS[CONF_ADAPT_UNTIL_SLEEP] = ( DOCS[CONF_ADAPT_UNTIL_SLEEP] = (
"This option ignores the current state of the sleep switch. "
"When enabled, Adaptive Lighting will treat sleep settings as the minimum, " "When enabled, Adaptive Lighting will treat sleep settings as the minimum, "
"transitioning to these values after sunset. 🌙" "transitioning color temperature to these values after sunset. 🌙"
) )
CONF_ADAPT_COLOR_TEMP_UNTIL_SLEEP, DEFAULT_ADAPT_COLOR_TEMP_UNTIL_SLEEP = (
"adapt_color_temp_until_sleep",
True,
)
DOCS[
CONF_ADAPT_COLOR_TEMP_UNTIL_SLEEP
] = "Only active when `transition_until_sleep` is true."
CONF_ADAPT_BRIGHTNESS_UNTIL_SLEEP, DEFAULT_ADAPT_BRIGHTNESS_UNTIL_SLEEP = (
"adapt_brightness_until_sleep",
False,
)
DOCS[
CONF_ADAPT_BRIGHTNESS_UNTIL_SLEEP
] = "Only active when `transition_until_sleep` is true."
CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0 CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0
DOCS[CONF_ADAPT_DELAY] = ( DOCS[CONF_ADAPT_DELAY] = (
@ -176,6 +210,15 @@ DOCS[CONF_AUTORESET_CONTROL] = (
"Set to 0 to disable. ⏲️" "Set to 0 to disable. ⏲️"
) )
CONF_WATCHED_LIGHTS, DEFAULT_WATCHED_LIGHTS = "watched_lights", {}
DOCS[CONF_WATCHED_LIGHTS] = (
"Use this dictionary of lights to check for manually controlled events"
" in addition to the main lights 🌟"
" Requires `alt_detect_method` to be True."
" Example: `watched_lights: {light.watch_light: light.main_light}` will fire"
" manually controlled events to `light.main_light`"
)
SLEEP_MODE_SWITCH = "sleep_mode_switch" SLEEP_MODE_SWITCH = "sleep_mode_switch"
ADAPT_COLOR_SWITCH = "adapt_color_switch" ADAPT_COLOR_SWITCH = "adapt_color_switch"
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch" ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
@ -196,11 +239,24 @@ DOCS[CONF_TURN_ON_LIGHTS] = "Whether to turn on lights that are currently off.
SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings" SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings"
CONF_USE_DEFAULTS = "use_defaults" CONF_USE_DEFAULTS = "use_defaults"
DOCS[CONF_USE_DEFAULTS] = ( DOCS[CONF_USE_DEFAULTS] = (
"Sets the default values not specified in this service call. Options: " "Where to autofill config options that are not passed to this service. Options: "
'"current" (default, retains current values), "factory" (resets to ' '"current" (default, retains current values), "factory" (resets to '
'documented defaults), or "configuration" (reverts to switch config defaults). ⚙️' 'documented defaults), or "configuration" (reverts to original user config). ⚙️'
) )
CONF_WHICH_SWITCH, DEFAULT_WHICH_SWITCH = "switch_type", "main"
DOCS[CONF_WHICH_SWITCH] = (
"Which switch to target in this service call. Options: "
'"main" (default, targets the main switch), "sleep", "brightness", "color"'
)
DOCS[
SERVICE_TURN_ON
] = "Turn on an Adaptive Lighting main/sleep/brightness/color switch"
DOCS[
SERVICE_TURN_OFF
] = "Turn off an Adaptive Lighting main/sleep/brightness/color switch"
DOCS[SERVICE_TOGGLE] = "Toggle an Adaptive Lighting main/sleep/brightness/color switch"
TURNING_OFF_DELAY = 5 TURNING_OFF_DELAY = 5
DOCS_MANUAL_CONTROL = { DOCS_MANUAL_CONTROL = {
@ -225,12 +281,15 @@ def int_between(min_int, max_int):
VALIDATION_TUPLES = [ VALIDATION_TUPLES = [
(CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids), (CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids),
(CONF_WATCHED_LIGHTS, DEFAULT_WATCHED_LIGHTS, dict),
(CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool), (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool),
(CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool), (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool),
(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION), (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
(CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION), (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION),
(CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION), (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION),
(CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool), (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool),
(CONF_ADAPT_BRIGHTNESS_UNTIL_SLEEP, DEFAULT_ADAPT_BRIGHTNESS_UNTIL_SLEEP, bool),
(CONF_ADAPT_COLOR_TEMP_UNTIL_SLEEP, DEFAULT_ADAPT_COLOR_TEMP_UNTIL_SLEEP, bool),
(CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int), (CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int),
(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)), (CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)),
(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)), (CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)),
@ -261,7 +320,9 @@ VALIDATION_TUPLES = [
(CONF_MIN_SUNSET_TIME, NONE_STR, str), (CONF_MIN_SUNSET_TIME, NONE_STR, str),
(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int), (CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int),
(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_FLAT_LIMITS, DEFAULT_FLAT_LIMITS, bool),
(CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool), (CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool),
(CONF_ALT_DETECT_METHOD, DEFAULT_ALT_DETECT_METHOD, bool),
(CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool), (CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool),
(CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool), (CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS, bool),
(CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)), (CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY, int_between(0, 10000)),
@ -273,8 +334,6 @@ VALIDATION_TUPLES = [
), ),
] ]
CONST_COLOR = "color"
def timedelta_as_int(value): def timedelta_as_int(value):
"""Convert a `datetime.timedelta` object to an integer. """Convert a `datetime.timedelta` object to an integer.
@ -323,28 +382,43 @@ _DOMAIN_SCHEMA = vol.Schema(
) )
def apply_service_schema(initial_transition: int = 1): SCHEMA_APPLY = vol.Schema(
"""Return the schema for the apply service.""" {
return vol.Schema( vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
{ vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_TRANSITION): VALID_TRANSITION,
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean,
vol.Optional( vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean,
CONF_TRANSITION, vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean,
default=initial_transition, vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean,
): VALID_TRANSITION, }
vol.Optional(ATTR_ADAPT_BRIGHTNESS, default=True): cv.boolean, )
vol.Optional(ATTR_ADAPT_COLOR, default=True): cv.boolean,
vol.Optional(CONF_PREFER_RGB_COLOR, default=False): cv.boolean,
vol.Optional(CONF_TURN_ON_LIGHTS, default=False): cv.boolean,
}
)
SCHEMA_SERVICE_TOGGLE = vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
vol.Optional(CONF_WHICH_SWITCH): cv.string,
}
)
SET_MANUAL_CONTROL_SCHEMA = vol.Schema( SCHEMA_SET_MANUAL_CONTROL = vol.Schema(
{ {
vol.Optional(CONF_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids, vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean, vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean,
} }
) )
SCHEMA_CHANGE_SWITCH_SETTINGS = vol.Schema(
{
vol.Optional(CONF_USE_DEFAULTS): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Required(CONF_LIGHTS, default=[]): [],
**{
vol.Optional(k): valid
for k, _, valid in VALIDATION_TUPLES
if k not in [CONF_INTERVAL, CONF_NAME, CONF_LIGHTS]
},
}
)

View file

@ -20,12 +20,15 @@
"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.", "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": { "data": {
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
"watched_lights": "watched_lights: Use this dictionary of lights to check for manually controlled events in addition to the main lights 🌟 Example: {light.watch_light: light.main_light} will fire manually controlled events to light.main_light",
"prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", "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`. 📝", "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. ⏲️", "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. 😴", "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": "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. 🌙", "transition_until_sleep": "transition_until_sleep: This option ignores the current state of the sleep switch. When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning color temperature to these values after sunset. 🌙",
"adapt_brightness_until_sleep": "adapt_brightness_until_sleep: Only active when `transition_until_sleep` is true.",
"adapt_color_temp_until_sleep": "adapt_color_temp_until_sleep: Only active when `transition_until_sleep` is true.",
"interval": "interval: Frequency to adapt the lights, in seconds. 🔄", "interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
"min_brightness": "min_brightness: Minimum brightness percentage. 💡", "min_brightness": "min_brightness: Minimum brightness percentage. 💡",
"max_brightness": "max_brightness: Maximum brightness percentage. 💡", "max_brightness": "max_brightness: Maximum brightness percentage. 💡",
@ -42,7 +45,9 @@
"min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", "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. ⏰", "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`). 🔄", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"flat_limits": "flat_limits: When True, will not calculate between the max/min supported limits of your light. Example: when adapting brightness to 50% while max_brightness is set to 80%, Adaptive Lighting will use 80% instead of 90%",
"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`! 🔒", "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`! 🔒",
"alt_detect_method": "alt_detect_method: alt_detect_method: When true, will check for any significant changes in the opposite direction of where adaptive-lighting tried to adapt last. This is an alternative to 'detect_non_ha_changes' (default: false)",
"detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", "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. 🔀", "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. ⏲️", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",

View file

@ -23,11 +23,7 @@ from homeassistant.components.light import (
ATTR_COLOR_NAME, ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN, ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR, ATTR_HS_COLOR,
ATTR_MAX_COLOR_TEMP_KELVIN,
ATTR_MIN_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR, ATTR_RGB_COLOR,
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES, ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION, ATTR_TRANSITION,
ATTR_XY_COLOR, ATTR_XY_COLOR,
@ -36,7 +32,6 @@ from homeassistant.components.light import (
COLOR_MODE_HS, COLOR_MODE_HS,
COLOR_MODE_RGB, COLOR_MODE_RGB,
COLOR_MODE_RGBW, COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_XY, COLOR_MODE_XY,
) )
from homeassistant.components.light import ( from homeassistant.components.light import (
@ -61,6 +56,7 @@ from homeassistant.const import (
EVENT_CALL_SERVICE, EVENT_CALL_SERVICE,
EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STARTED,
EVENT_STATE_CHANGED, EVENT_STATE_CHANGED,
SERVICE_TOGGLE,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_OFF, STATE_OFF,
@ -74,9 +70,10 @@ from homeassistant.core import (
HomeAssistant, HomeAssistant,
ServiceCall, ServiceCall,
State, State,
async_get_hass,
callback, callback,
) )
from homeassistant.helpers import entity_platform, entity_registry from homeassistant.helpers import entity_registry
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change_event, async_track_state_change_event,
@ -94,7 +91,6 @@ from homeassistant.util.color import (
) )
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
import ulid_transform import ulid_transform
import voluptuous as vol
from .const import ( from .const import (
ADAPT_BRIGHTNESS_SWITCH, ADAPT_BRIGHTNESS_SWITCH,
@ -102,10 +98,14 @@ from .const import (
ATTR_ADAPT_BRIGHTNESS, ATTR_ADAPT_BRIGHTNESS,
ATTR_ADAPT_COLOR, ATTR_ADAPT_COLOR,
ATTR_TURN_ON_OFF_LISTENER, ATTR_TURN_ON_OFF_LISTENER,
CONF_ADAPT_BRIGHTNESS_UNTIL_SLEEP,
CONF_ADAPT_COLOR_TEMP_UNTIL_SLEEP,
CONF_ADAPT_DELAY, CONF_ADAPT_DELAY,
CONF_ADAPT_UNTIL_SLEEP, CONF_ADAPT_UNTIL_SLEEP,
CONF_ALT_DETECT_METHOD,
CONF_AUTORESET_CONTROL, CONF_AUTORESET_CONTROL,
CONF_DETECT_NON_HA_CHANGES, CONF_DETECT_NON_HA_CHANGES,
CONF_FLAT_LIMITS,
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, CONF_INCLUDE_CONFIG_IN_ATTRIBUTES,
CONF_INITIAL_TRANSITION, CONF_INITIAL_TRANSITION,
CONF_INTERVAL, CONF_INTERVAL,
@ -134,23 +134,26 @@ from .const import (
CONF_TRANSITION, CONF_TRANSITION,
CONF_TURN_ON_LIGHTS, CONF_TURN_ON_LIGHTS,
CONF_USE_DEFAULTS, CONF_USE_DEFAULTS,
CONST_COLOR, CONF_WATCHED_LIGHTS,
CONF_WHICH_SWITCH,
DOMAIN, DOMAIN,
EXTRA_VALIDATION, EXTRA_VALIDATION,
ICON_BRIGHTNESS, ICON_BRIGHTNESS,
ICON_COLOR_TEMP, ICON_COLOR_TEMP,
ICON_MAIN, ICON_MAIN,
ICON_SLEEP, ICON_SLEEP,
SCHEMA_APPLY,
SCHEMA_CHANGE_SWITCH_SETTINGS,
SCHEMA_SERVICE_TOGGLE,
SCHEMA_SET_MANUAL_CONTROL,
SERVICE_APPLY, SERVICE_APPLY,
SERVICE_CHANGE_SWITCH_SETTINGS, SERVICE_CHANGE_SWITCH_SETTINGS,
SERVICE_SET_MANUAL_CONTROL, SERVICE_SET_MANUAL_CONTROL,
SET_MANUAL_CONTROL_SCHEMA,
SLEEP_MODE_SWITCH, SLEEP_MODE_SWITCH,
SUN_EVENT_MIDNIGHT, SUN_EVENT_MIDNIGHT,
SUN_EVENT_NOON, SUN_EVENT_NOON,
TURNING_OFF_DELAY, TURNING_OFF_DELAY,
VALIDATION_TUPLES, VALIDATION_TUPLES,
apply_service_schema,
replace_none_str, replace_none_str,
) )
@ -161,7 +164,6 @@ _SUPPORT_OPTS = {
ATTR_TRANSITION: SUPPORT_TRANSITION, ATTR_TRANSITION: SUPPORT_TRANSITION,
} }
VALID_COLOR_MODES = { VALID_COLOR_MODES = {
COLOR_MODE_BRIGHTNESS: ATTR_BRIGHTNESS, COLOR_MODE_BRIGHTNESS: ATTR_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP_KELVIN, COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP_KELVIN,
@ -303,9 +305,9 @@ def _split_service_data(service_data, adapt_brightness, adapt_color):
def _get_switches_with_lights( def _get_switches_with_lights(
hass: HomeAssistant, lights: list[str] hass: HomeAssistant, lights: list[str] | None = None
) -> list[AdaptiveSwitch]: ) -> list[AdaptiveSwitch]:
"""Get all switches that control at least one of the lights passed.""" """Get all switches. If lights is defined, return only switches found with these lights."""
config_entries = hass.config_entries.async_entries(DOMAIN) config_entries = hass.config_entries.async_entries(DOMAIN)
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
switches = [] switches = []
@ -314,10 +316,13 @@ def _get_switches_with_lights(
if entry is None: # entry might be disabled and therefore missing if entry is None: # entry might be disabled and therefore missing
continue continue
switch = data[config.entry_id]["instance"] switch = data[config.entry_id]["instance"]
all_check_lights = _expand_light_groups(hass, lights) if lights:
switch._expand_light_groups() all_check_lights = _expand_light_groups(hass, lights)
# Check if any of the lights are in the switch's lights switch._expand_light_groups()
if set(switch._lights) & set(all_check_lights): # Check if any of the lights are in the switch's lights
if set(switch._lights) & set(all_check_lights):
switches.append(switch)
else:
switches.append(switch) switches.append(switch)
return switches return switches
@ -359,13 +364,7 @@ def _get_switches_from_service_call(
switch_entity_ids: list[str] | None = data.get("entity_id") switch_entity_ids: list[str] | None = data.get("entity_id")
if not lights and not switch_entity_ids: if not lights and not switch_entity_ids:
raise ValueError( return _get_switches_with_lights(hass)
"adaptive-lighting: Neither a switch nor a light was provided in the service call."
" If you intend to adapt all lights on all switches, please inform the developers at"
" https://github.com/basnijholt/adaptive-lighting about your use case."
" Currently, you must pass either an adaptive-lighting switch or the lights to an"
" `adaptive_lighting` service call."
)
if switch_entity_ids is not None: if switch_entity_ids is not None:
if len(switch_entity_ids) > 1 and lights: if len(switch_entity_ids) > 1 and lights:
@ -391,44 +390,189 @@ def _get_switches_from_service_call(
) )
async def handle_change_switch_settings( @callback
switch: AdaptiveSwitch, service_call: ServiceCall async def handle_apply(service_call: ServiceCall):
) -> None: """Handle the entity service apply."""
"""Allows HASS to change config values via a service call.""" hass = async_get_hass()
data = service_call.data data = service_call.data
_LOGGER.debug(
which = data.get(CONF_USE_DEFAULTS, "current") "Called 'adaptive_lighting.apply' service with '%s'",
if which == "current": # use whatever we're already using. data,
defaults = switch._current_settings # pylint: disable=protected-access
elif which == "factory": # use actual defaults listed in the documentation
defaults = {key: default for key, default, _ in VALIDATION_TUPLES}
elif which == "configuration":
# use whatever's in the config flow or configuration.yaml
defaults = switch._config_backup # pylint: disable=protected-access
else:
defaults = None
switch._set_changeable_settings(
data=data,
defaults=defaults,
) )
switches = _get_switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
all_lights = switch._lights # pylint: disable=protected-access
else:
all_lights = _expand_light_groups(switch.hass, lights)
switch.turn_on_off_listener.lights.update(all_lights)
for light in all_lights:
transition = data.get(CONF_TRANSITION)
if not data[CONF_TURN_ON_LIGHTS]:
if not is_on(hass, light):
continue
if not transition:
transition = switch._transition # pylint: disable=protected-access
elif not transition:
transition = (
switch._initial_transition
) # pylint: disable=protected-access
await switch._adapt_light( # pylint: disable=protected-access
light,
transition,
data[ATTR_ADAPT_BRIGHTNESS],
data[ATTR_ADAPT_COLOR],
data[CONF_PREFER_RGB_COLOR],
force=True,
context=switch.create_context("service", parent=service_call.context),
)
@callback
async def handle_set_manual_control(service_call: ServiceCall):
"""Set or unset lights as 'manually controlled'."""
hass = async_get_hass()
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.set_manual_control' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
all_lights = switch._lights # pylint: disable=protected-access
else:
all_lights = _expand_light_groups(switch.hass, lights)
if service_call.data[CONF_MANUAL_CONTROL]:
for light in all_lights:
_fire_manual_control_event(switch, light, service_call.context)
else:
switch.turn_on_off_listener.reset(*all_lights)
if switch.is_on:
# pylint: disable=protected-access
await switch._update_attrs_and_maybe_adapt_lights(
all_lights,
transition=switch._initial_transition,
force=True,
context=switch.create_context(
"service", parent=service_call.context
),
)
@callback
async def handle_change_switch_settings(service_call: ServiceCall) -> None:
"""Allows HASS to change config values via a service call."""
hass = async_get_hass()
data = service_call.data
_LOGGER.debug( _LOGGER.debug(
"Called 'adaptive_lighting.change_switch_settings' service with '%s'", "Called 'adaptive_lighting.change_switch_settings' service with '%s'",
data, data,
) )
all_lights = switch._lights # pylint: disable=protected-access switches = _get_switches_from_service_call(hass, service_call)
switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False) for switch in switches:
if switch.is_on: # which denotes where to autofill blank config options.
which = data.get(CONF_USE_DEFAULTS, "current")
if which == "current":
# use whatever we're already using.
defaults = switch._current_settings # pylint: disable=protected-access
elif which == "factory":
# use actual defaults listed in the documentation
defaults = {key: default for key, default, _ in VALIDATION_TUPLES}
elif which == "configuration":
# use whatever's in the config flow or configuration.yaml
defaults = switch._config_backup # pylint: disable=protected-access
else:
defaults = None
switch._set_changeable_settings(
data=data,
defaults=defaults,
)
all_lights = switch._lights # pylint: disable=protected-access
switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False)
if not switch.is_on:
continue
await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access
all_lights, all_lights,
transition=switch._initial_transition, transition=switch._transition,
force=True, force=True,
context=switch.create_context("service", parent=service_call.context), context=switch.create_context("service", parent=service_call.context),
) )
@callback
async def handle_turn_on(service_call: ServiceCall):
"""Toggles the specified switch."""
hass = async_get_hass()
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.turn_on' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
if data[CONF_WHICH_SWITCH] == "sleep":
switches = [s.sleep_mode_switch for s in switches]
elif data[CONF_WHICH_SWITCH] == "brightness":
switches = [s.adapt_brightness_switch for s in switches]
elif data[CONF_WHICH_SWITCH] == "color":
switches = [s.adapt_color_switch for s in switches]
_LOGGER.debug("Turning on switches [%s]", switches)
for switch in switches:
await switch.async_turn_on()
@callback
async def handle_turn_off(service_call: ServiceCall):
"""Toggles the specified switch."""
hass = async_get_hass()
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.turn_off' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
if data[CONF_WHICH_SWITCH] == "sleep":
switches = [s.sleep_mode_switch for s in switches]
elif data[CONF_WHICH_SWITCH] == "brightness":
switches = [s.adapt_brightness_switch for s in switches]
elif data[CONF_WHICH_SWITCH] == "color":
switches = [s.adapt_color_switch for s in switches]
_LOGGER.debug("Turning off switches [%s]", switches)
for switch in switches:
await switch.async_turn_off()
@callback
async def handle_toggle(service_call: ServiceCall):
"""Toggles the specified switch."""
hass = async_get_hass()
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.toggle' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
if data[CONF_WHICH_SWITCH] == "sleep":
switches = [s.sleep_mode_switch for s in switches]
elif data[CONF_WHICH_SWITCH] == "brightness":
switches = [s.adapt_brightness_switch for s in switches]
elif data[CONF_WHICH_SWITCH] == "color":
switches = [s.adapt_color_switch for s in switches]
_LOGGER.debug("Toggling switches [%s]", switches)
for switch in switches:
if switch.is_on:
await switch.async_turn_off()
else:
await switch.async_turn_on()
@callback @callback
def _fire_manual_control_event( def _fire_manual_control_event(
switch: AdaptiveSwitch, light: str, context: Context, is_async=True switch: AdaptiveSwitch, light: str, context: Context, is_async=True
@ -490,75 +634,36 @@ async def async_setup_entry(
update_before_add=True, update_before_add=True,
) )
@callback
async def handle_apply(service_call: ServiceCall):
"""Handle the entity service apply."""
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.apply' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
all_lights = switch._lights # pylint: disable=protected-access
else:
all_lights = _expand_light_groups(switch.hass, lights)
switch.turn_on_off_listener.lights.update(all_lights)
for light in all_lights:
if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light):
await switch._adapt_light( # pylint: disable=protected-access
light,
data[CONF_TRANSITION],
data[ATTR_ADAPT_BRIGHTNESS],
data[ATTR_ADAPT_COLOR],
data[CONF_PREFER_RGB_COLOR],
force=True,
context=switch.create_context(
"service", parent=service_call.context
),
)
@callback
async def handle_set_manual_control(service_call: ServiceCall):
"""Set or unset lights as 'manually controlled'."""
data = service_call.data
_LOGGER.debug(
"Called 'adaptive_lighting.set_manual_control' service with '%s'",
data,
)
switches = _get_switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
all_lights = switch._lights # pylint: disable=protected-access
else:
all_lights = _expand_light_groups(switch.hass, lights)
if service_call.data[CONF_MANUAL_CONTROL]:
for light in all_lights:
_fire_manual_control_event(switch, light, service_call.context)
else:
switch.turn_on_off_listener.reset(*all_lights)
if switch.is_on:
# pylint: disable=protected-access
await switch._update_attrs_and_maybe_adapt_lights(
all_lights,
transition=switch._initial_transition,
force=True,
context=switch.create_context(
"service", parent=service_call.context
),
)
# Register `apply` service # Register `apply` service
hass.services.async_register( hass.services.async_register(
domain=DOMAIN, domain=DOMAIN,
service=SERVICE_APPLY, service=SERVICE_APPLY,
service_func=handle_apply, service_func=handle_apply,
schema=apply_service_schema( schema=SCHEMA_APPLY,
switch._initial_transition )
), # pylint: disable=protected-access
# Register `turn_on` service
hass.services.async_register(
domain=DOMAIN,
service=SERVICE_TURN_ON,
service_func=handle_turn_on,
schema=SCHEMA_SERVICE_TOGGLE,
)
# Register `turn_off` service
hass.services.async_register(
domain=DOMAIN,
service=SERVICE_TURN_OFF,
service_func=handle_turn_off,
schema=SCHEMA_SERVICE_TOGGLE,
)
# Register `toggle` service
hass.services.async_register(
domain=DOMAIN,
service=SERVICE_TOGGLE,
service_func=handle_toggle,
schema=SCHEMA_SERVICE_TOGGLE,
) )
# Register `set_manual_control` service # Register `set_manual_control` service
@ -566,20 +671,15 @@ async def async_setup_entry(
domain=DOMAIN, domain=DOMAIN,
service=SERVICE_SET_MANUAL_CONTROL, service=SERVICE_SET_MANUAL_CONTROL,
service_func=handle_set_manual_control, service_func=handle_set_manual_control,
schema=SET_MANUAL_CONTROL_SCHEMA, schema=SCHEMA_SET_MANUAL_CONTROL,
) )
args = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string} # Register `change_switch_settings` service
# Modifying these after init isn't possible hass.services.async_register(
skip = (CONF_INTERVAL, CONF_NAME, CONF_LIGHTS) domain=DOMAIN,
for k, _, valid in VALIDATION_TUPLES: service=SERVICE_CHANGE_SWITCH_SETTINGS,
if k not in skip: service_func=handle_change_switch_settings,
args[vol.Optional(k)] = valid schema=SCHEMA_CHANGE_SWITCH_SETTINGS,
platform = entity_platform.current_platform.get()
platform.async_register_entity_service(
SERVICE_CHANGE_SWITCH_SETTINGS,
args,
handle_change_switch_settings,
) )
@ -658,37 +758,6 @@ def _supported_to_attributes(supported):
return supported_attributes, supports_colors return supported_attributes, supports_colors
def _supported_features(hass: HomeAssistant, light: str):
state = hass.states.get(light)
legacy_supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
legacy_supported = {
key for key, value in _SUPPORT_OPTS.items() if legacy_supported_features & value
}
supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set())
supported, supports_colors = _supported_to_attributes(
legacy_supported.union(supported_color_modes)
)
min_kelvin = state.attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN)
max_kelvin = state.attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN)
supported.update(
{
ATTR_MIN_COLOR_TEMP_KELVIN: min_kelvin,
ATTR_MAX_COLOR_TEMP_KELVIN: max_kelvin,
}
)
if supports_colors:
# Adding brightness here, see
# comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011
supported[ATTR_BRIGHTNESS] = True
if CONST_COLOR not in legacy_supported:
# supports_colors = False
_LOGGER.debug(
"'supported_color_modes' supports color but the legacy 'supported_features'"
" bitfield says we do not. Despite this we'll assume light '%s' supports colors",
)
return supported, supports_colors
def color_difference_redmean( def color_difference_redmean(
rgb1: tuple[float, float, float], rgb2: tuple[float, float, float] rgb1: tuple[float, float, float], rgb2: tuple[float, float, float]
) -> float: ) -> float:
@ -708,6 +777,15 @@ def color_difference_redmean(
return math.sqrt(red_term + green_term + blue_term) return math.sqrt(red_term + green_term + blue_term)
def check_direction_change(last: int, current: int, last_adapt_value: int) -> bool:
_LOGGER.debug("compare direction: current value %s to last value %s", current, last)
if last_adapt_value < last: # Value adapting down
return current > last or current < last_adapt_value
elif last_adapt_value > last: # Value adapting up
return current < last or current > last_adapt_value
return False
# All comparisons should be done with RGB since # All comparisons should be done with RGB since
# converting anything to color temp is inaccurate. # converting anything to color temp is inaccurate.
def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]: def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]:
@ -750,6 +828,7 @@ def _attributes_have_changed(
adapt_brightness: bool, adapt_brightness: bool,
adapt_color: bool, adapt_color: bool,
context: Context, context: Context,
last_adapt_attempt=None,
) -> bool: ) -> bool:
if adapt_color: if adapt_color:
old_attributes, new_attributes = _add_missing_attributes( old_attributes, new_attributes = _add_missing_attributes(
@ -764,15 +843,33 @@ def _attributes_have_changed(
last_brightness = old_attributes[ATTR_BRIGHTNESS] last_brightness = old_attributes[ATTR_BRIGHTNESS]
current_brightness = new_attributes[ATTR_BRIGHTNESS] current_brightness = new_attributes[ATTR_BRIGHTNESS]
if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE: if abs(current_brightness - last_brightness) > BRIGHTNESS_CHANGE:
_LOGGER.debug( if last_adapt_attempt:
"Brightness of '%s' significantly changed from %s to %s with" changed = check_direction_change(
" context.id='%s'", last_brightness,
light, current_brightness,
last_brightness, last_adapt_attempt[ATTR_BRIGHTNESS],
current_brightness, )
context.id, _LOGGER.debug(
) "altdetect: Brightness of '%s' changed from %s to %s intended %s with"
return True " context.id='%s' Significant? %s",
light,
last_brightness,
current_brightness,
last_adapt_attempt[ATTR_BRIGHTNESS],
context.id,
changed,
)
return changed
else:
_LOGGER.debug(
"Brightness of '%s' significantly changed from %s to %s with"
" context.id='%s'",
light,
last_brightness,
current_brightness,
context.id,
)
return True
if ( if (
adapt_color adapt_color
@ -782,15 +879,33 @@ def _attributes_have_changed(
last_color_temp = old_attributes[ATTR_COLOR_TEMP_KELVIN] last_color_temp = old_attributes[ATTR_COLOR_TEMP_KELVIN]
current_color_temp = new_attributes[ATTR_COLOR_TEMP_KELVIN] current_color_temp = new_attributes[ATTR_COLOR_TEMP_KELVIN]
if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE: if abs(current_color_temp - last_color_temp) > COLOR_TEMP_CHANGE:
_LOGGER.debug( if last_adapt_attempt:
"Color temperature of '%s' significantly changed from %s to %s with" changed = check_direction_change(
" context.id='%s'", last_color_temp,
light, current_color_temp,
last_color_temp, last_adapt_attempt[ATTR_COLOR_TEMP_KELVIN],
current_color_temp, )
context.id, _LOGGER.debug(
) "altdetect: Color temperature of '%s' changed from %s to %s intended %s with"
return True " context.id='%s' Significant? %s",
light,
last_color_temp,
current_color_temp,
last_adapt_attempt[ATTR_COLOR_TEMP_KELVIN],
context.id,
changed,
)
return changed
else:
_LOGGER.debug(
"Color temperature of '%s' significantly changed from %s to %s with"
" context.id='%s'",
light,
last_color_temp,
current_color_temp,
context.id,
)
return True
if ( if (
adapt_color adapt_color
@ -910,13 +1025,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._transition = data[CONF_TRANSITION] self._transition = data[CONF_TRANSITION]
self._adapt_delay = data[CONF_ADAPT_DELAY] self._adapt_delay = data[CONF_ADAPT_DELAY]
self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] self._send_split_delay = data[CONF_SEND_SPLIT_DELAY]
self._watched_lights = data[CONF_WATCHED_LIGHTS]
self._take_over_control = data[CONF_TAKE_OVER_CONTROL] self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
self._alt_detect_method = data[CONF_ALT_DETECT_METHOD]
self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] 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]: if not data[CONF_TAKE_OVER_CONTROL] and (
_LOGGER.warning( data[CONF_ALT_DETECT_METHOD] or data[CONF_DETECT_NON_HA_CHANGES]
"%s: Config mismatch: 'detect_non_ha_changes: true' " ):
"requires 'take_over_control' to be enabled. Adjusting config " _LOGGER.warn(
"and continuing setup with `take_over_control: true`.", "%s: Config mismatch: 'alt_detect_method: true'"
" OR 'detect_non_ha_changes: true' are set in config, however required"
" variable 'take_over_control' is turned off. Please check your"
" configuration to ensure desired functionality. We will now"
" enable 'take_over_control' and continue setting up the"
" adaptive-lighting integration normally.",
self._name, self._name,
) )
self._take_over_control = True self._take_over_control = True
@ -933,7 +1055,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._sun_light_settings = SunLightSettings( self._sun_light_settings = SunLightSettings(
name=self._name, name=self._name,
astral_location=location, astral_location=location,
adapt_brightness_until_sleep=data[CONF_ADAPT_BRIGHTNESS_UNTIL_SLEEP],
adapt_color_temp_until_sleep=data[CONF_ADAPT_COLOR_TEMP_UNTIL_SLEEP],
adapt_until_sleep=data[CONF_ADAPT_UNTIL_SLEEP], adapt_until_sleep=data[CONF_ADAPT_UNTIL_SLEEP],
flat_limits=data[CONF_FLAT_LIMITS],
max_brightness=data[CONF_MAX_BRIGHTNESS], max_brightness=data[CONF_MAX_BRIGHTNESS],
max_color_temp=data[CONF_MAX_COLOR_TEMP], max_color_temp=data[CONF_MAX_COLOR_TEMP],
min_brightness=data[CONF_MIN_BRIGHTNESS], min_brightness=data[CONF_MIN_BRIGHTNESS],
@ -1141,12 +1266,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# Build service data. # Build service data.
service_data = {ATTR_ENTITY_ID: light} service_data = {ATTR_ENTITY_ID: light}
features, supports_colors = _supported_features(self.hass, light) features = _supported_features(self.hass, light)
# Check transition == 0 to fix #378 # Check transition == 0 to fix #378
if ATTR_TRANSITION in features and transition > 0: if "transition" in features and transition > 0:
service_data[ATTR_TRANSITION] = transition service_data[ATTR_TRANSITION] = transition
if ATTR_BRIGHTNESS in features and adapt_brightness: if "brightness" in features and adapt_brightness:
brightness = round(255 * self._settings["brightness_pct"] / 100) brightness = round(255 * self._settings["brightness_pct"] / 100)
service_data[ATTR_BRIGHTNESS] = brightness service_data[ATTR_BRIGHTNESS] = brightness
@ -1155,26 +1280,31 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color" and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color"
) )
if ( if (
ATTR_COLOR_TEMP_KELVIN in features "color_temp" in features
and adapt_color and adapt_color
and not (prefer_rgb_color and supports_colors) and not (prefer_rgb_color and "color" in features)
and not (sleep_rgb and supports_colors) and not (sleep_rgb and "color" in features)
): ):
_LOGGER.debug("%s: Setting color_temp of light %s", self._name, light) _LOGGER.debug("%s: Setting color_temp of light %s", self._name, light)
min_kelvin = features[ATTR_MIN_COLOR_TEMP_KELVIN] attributes = self.hass.states.get(light).attributes
max_kelvin = features[ATTR_MAX_COLOR_TEMP_KELVIN] min_kelvin = attributes["min_color_temp_kelvin"]
max_kelvin = attributes["max_color_temp_kelvin"]
color_temp_kelvin = self._settings["color_temp_kelvin"] color_temp_kelvin = self._settings["color_temp_kelvin"]
color_temp_kelvin = max(min(color_temp_kelvin, max_kelvin), min_kelvin) color_temp_kelvin = max(min(color_temp_kelvin, max_kelvin), min_kelvin)
service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin
elif supports_colors and adapt_color: elif "color" in features and adapt_color:
_LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light) _LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light)
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"] service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
# Check if service data differs from the last. See #80.
context = context or self.create_context("adapt_lights") listener = self.turn_on_off_listener
last_service_data = listener.last_service_data.get(light)
# See #80. Doesn't check if transitions differ but it does the job. ignore_fields = {ATTR_TRANSITION}
last_service_data = self.turn_on_off_listener.last_service_data if (
if not force and last_service_data.get(light) == service_data: not force
and last_service_data
and {k for k, _ in last_service_data.items() ^ service_data.items()}
== ignore_fields
):
_LOGGER.debug( _LOGGER.debug(
"%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')", "%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')",
self._name, self._name,
@ -1182,8 +1312,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context.id, context.id,
) )
return return
else: listener.last_service_data[light] = service_data
self.turn_on_off_listener.last_service_data[light] = service_data
context = context or self.create_context("adapt_lights")
async def turn_on(service_data): async def turn_on(service_data):
_LOGGER.debug( _LOGGER.debug(
@ -1270,6 +1401,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
transition: int | None, transition: int | None,
force: bool, force: bool,
context: Context | None, context: Context | None,
adapt_brightness: bool | None = None,
adapt_color: bool | None = None,
) -> None: ) -> None:
assert context is not None assert context is not None
_LOGGER.debug( _LOGGER.debug(
@ -1284,13 +1417,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
adapt_brightness = self.adapt_brightness_switch.is_on adapt_brightness = self.adapt_brightness_switch.is_on
adapt_color = self.adapt_color_switch.is_on adapt_color = self.adapt_color_switch.is_on
for light in lights: all_lights = {k: k for k in lights}
if not is_on(self.hass, light): all_lights.update(self._watched_lights)
for wlight, mlight in all_lights.items():
if not is_on(self.hass, mlight):
continue continue
manually_controlled = self.turn_on_off_listener.is_manually_controlled( manually_controlled = self.turn_on_off_listener.is_manually_controlled(
self, self,
light, wlight,
mlight,
force, force,
adapt_brightness, adapt_brightness,
adapt_color, adapt_color,
@ -1301,7 +1438,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and not force and not force
and await self.turn_on_off_listener.significant_change( and await self.turn_on_off_listener.significant_change(
self, self,
light, wlight,
mlight,
adapt_brightness, adapt_brightness,
adapt_color, adapt_color,
context, context,
@ -1313,13 +1451,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
_LOGGER.debug( _LOGGER.debug(
"%s: '%s' is being manually controlled, stop adapting, context.id=%s.", "%s: '%s' is being manually controlled, stop adapting, context.id=%s.",
self._name, self._name,
light, mlight,
context.id, context.id,
) )
else: else:
_fire_manual_control_event(self, light, context) _fire_manual_control_event(self, mlight, context)
else: else:
await self._adapt_light(light, transition, force=force, context=context) await self._adapt_light(
wlight, transition, force=force, context=context
)
async def _sleep_mode_switch_state_event(self, event: Event) -> None: async def _sleep_mode_switch_state_event(self, event: Event) -> None:
if not match_switch_state_event(event, (STATE_ON, STATE_OFF)): if not match_switch_state_event(event, (STATE_ON, STATE_OFF)):
@ -1472,7 +1612,10 @@ class SunLightSettings:
name: str name: str
astral_location: astral.Location astral_location: astral.Location
adapt_brightness_until_sleep: bool
adapt_color_temp_until_sleep: bool
adapt_until_sleep: bool adapt_until_sleep: bool
flat_limits: bool
max_brightness: int max_brightness: int
max_color_temp: int max_color_temp: int
min_brightness: int min_brightness: int
@ -1507,15 +1650,27 @@ class SunLightSettings:
def calculate_noon_and_midnight( def calculate_noon_and_midnight(
sunset: datetime.datetime, sunrise: datetime.datetime sunset: datetime.datetime, sunrise: datetime.datetime
) -> tuple[datetime.datetime, datetime.datetime]: ) -> tuple[datetime.datetime, datetime.datetime]:
middle = abs(sunset - sunrise) / 2 total = abs(sunset - sunrise)
middle = total / 2
total = (
total.total_seconds() / 60 / 60 * (2 / 3)
) # about 12 hours normally.
_LOGGER.debug(
"Calculate noon/midnight. Total diff: %s, middle: %s", total, middle
)
if sunset > sunrise: if sunset > sunrise:
noon = sunrise + middle noon = sunrise + middle
midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1) midnight = noon + timedelta(hours=total) * (
1 if noon.hour < total else -1
)
else: else:
midnight = sunset + middle midnight = sunset + middle
noon = midnight + timedelta(hours=12) * ( noon = midnight + timedelta(hours=total) * (
1 if midnight.hour < 12 else -1 1 if midnight.hour < total else -1
) )
_LOGGER.debug(
"Calculate noon/midnight. Noon: %s Midnight: %s", noon, midnight
)
return noon, midnight return noon, midnight
location = self.astral_location location = self.astral_location
@ -1612,8 +1767,18 @@ class SunLightSettings:
return self.sleep_brightness return self.sleep_brightness
if percent > 0: if percent > 0:
return self.max_brightness return self.max_brightness
if self.adapt_until_sleep and self.adapt_brightness_until_sleep and percent < 0:
delta_brightness = abs(self.min_brightness - self.sleep_brightness)
return (delta_brightness * abs(1 + percent)) + self.sleep_brightness
delta_brightness = self.max_brightness - self.min_brightness delta_brightness = self.max_brightness - self.min_brightness
percent = 1 + percent percent = 1 + percent
if self.flat_limits:
if percent * 100 > self.max_brightness:
return self.max_brightness
elif percent * 100 < self.min_brightness:
return self.min_brightness
return percent * 100
delta_brightness = self.max_brightness - self.min_brightness
return (delta_brightness * percent) + self.min_brightness return (delta_brightness * percent) + self.min_brightness
def calc_color_temp_kelvin(self, percent: float) -> int: def calc_color_temp_kelvin(self, percent: float) -> int:
@ -1624,7 +1789,7 @@ class SunLightSettings:
return 5 * round(ct / 5) # round to nearest 5 return 5 * round(ct / 5) # round to nearest 5
if percent == 0 or not self.adapt_until_sleep: if percent == 0 or not self.adapt_until_sleep:
return self.min_color_temp return self.min_color_temp
if self.adapt_until_sleep and percent < 0: if self.adapt_until_sleep and self.adapt_color_temp_until_sleep and percent < 0:
delta = abs(self.min_color_temp - self.sleep_color_temp) delta = abs(self.min_color_temp - self.sleep_color_temp)
ct = (delta * abs(1 + percent)) + self.sleep_color_temp ct = (delta * abs(1 + percent)) + self.sleep_color_temp
return 5 * round(ct / 5) # round to nearest 5 return 5 * round(ct / 5) # round to nearest 5
@ -1922,18 +2087,19 @@ class TurnOnOffListener:
def is_manually_controlled( def is_manually_controlled(
self, self,
switch: AdaptiveSwitch, switch: AdaptiveSwitch,
light: str, wlight: str,
mlight: str,
force: bool, force: bool,
adapt_brightness: bool, adapt_brightness: bool,
adapt_color: bool, adapt_color: bool,
) -> bool: ) -> bool:
"""Check if the light has been 'on' and is now manually controlled.""" """Check if the light has been 'on' and is now manually controlled."""
manual_control = self.manual_control.setdefault(light, False) manual_control = self.manual_control.setdefault(wlight, False)
if manual_control: if manual_control:
# Manually controlled until light is turned on and off # Manually controlled until light is turned on and off
return True return True
turn_on_event = self.turn_on_event.get(light) turn_on_event = self.turn_on_event.get(wlight)
if ( if (
turn_on_event is not None turn_on_event is not None
and not is_our_context(turn_on_event.context) and not is_our_context(turn_on_event.context)
@ -1946,13 +2112,13 @@ class TurnOnOffListener:
# Light was already on and 'light.turn_on' was not called by # Light was already on and 'light.turn_on' was not called by
# the adaptive_lighting integration. # the adaptive_lighting integration.
manual_control = True manual_control = True
_fire_manual_control_event(switch, light, turn_on_event.context) _fire_manual_control_event(switch, wlight, turn_on_event.context)
_LOGGER.debug( _LOGGER.debug(
"'%s' was already on and 'light.turn_on' was not called by the" "'%s' was already on and 'light.turn_on' was not called by the"
" adaptive_lighting integration (context.id='%s'), the Adaptive" " adaptive_lighting integration (context.id='%s'), the Adaptive"
" Lighting will stop adapting the light until the switch or the" " Lighting will stop adapting the light until the switch or the"
" light turns off and then on again.", " light turns off and then on again.",
light, mlight,
turn_on_event.context.id, turn_on_event.context.id,
) )
return manual_control return manual_control
@ -1960,7 +2126,8 @@ class TurnOnOffListener:
async def significant_change( async def significant_change(
self, self,
switch: AdaptiveSwitch, switch: AdaptiveSwitch,
light: str, wlight: str,
mlight: str,
adapt_brightness: bool, adapt_brightness: bool,
adapt_color: bool, adapt_color: bool,
context: Context, context: Context,
@ -1972,35 +2139,77 @@ class TurnOnOffListener:
detected, we mark the light as 'manually controlled' until the light detected, we mark the light as 'manually controlled' until the light
or switch is turned 'off' and 'on' again. or switch is turned 'off' and 'on' again.
""" """
last_service_data = self.last_service_data.get(light) last_service_data = self.last_service_data.get(wlight)
if last_service_data is None: if last_service_data is None:
return return
compare_to = functools.partial( compare_to = functools.partial(
_attributes_have_changed, _attributes_have_changed,
light=light, light=wlight,
adapt_brightness=adapt_brightness, adapt_brightness=adapt_brightness,
adapt_color=adapt_color, adapt_color=adapt_color,
context=context, context=context,
) )
if switch._alt_detect_method or wlight != mlight:
old_states: list[State] = self.last_state_change[wlight]
_LOGGER.debug("Total state changes detected: %s", len(old_states))
_LOGGER.debug(
"%s: 'alt_detect_method: true', check all state changes made to light %s",
switch._name,
wlight,
)
for index, old_state in enumerate(old_states):
# The first entry of old_states should always be the
# same as last_service_data[light], and can be ignored.
if index <= 1:
continue
_LOGGER.debug(
"%s: checking for a manual change between index %s and %s...",
switch._name,
index,
index - 1,
)
prior_state = old_states[index - 1]
if compare_to(
old_attributes=prior_state.attributes,
new_attributes=old_state.attributes,
last_adapt_attempt=last_service_data,
):
_LOGGER.info(
"Found unexpected state_change event for %s nr. %s (context.id=%s)"
" old_state=%s\nprior_state=%s",
wlight,
index,
context.id,
old_state,
prior_state,
)
_LOGGER.info(
"We will now set %s as manually controlled. (context.id=%s)",
wlight,
context.id,
)
return True
# Update state and check for a manual change not done in HA. # Update state and check for a manual change not done in HA.
# Ensure HASS is correctly updating your light's state with # Ensure HASS is correctly updating your light's state with
# light.turn_on calls if any problems arise. This # light.turn_on calls if any problems arise. This
# can happen e.g. using zigbee2mqtt with 'report: false' in device settings. # can happen e.g. using zigbee2mqtt with 'report: false' in device settings.
if switch._detect_non_ha_changes: if switch._detect_non_ha_changes:
if wlight != mlight:
return # only supported with alt_detect_method
_LOGGER.debug( _LOGGER.debug(
"%s: 'detect_non_ha_changes: true', calling update_entity(%s)" "%s: 'detect_non_ha_changes: true', calling update_entity(%s)"
" and check if it's last adapt succeeded.", " and check if it's last adapt succeeded.",
switch._name, switch._name,
light, wlight,
) )
# This update_entity probably isn't necessary now that we're checking # This update_entity probably isn't necessary now that we're checking
# if transitions finished from our last adapt. # if transitions finished from our last adapt.
await self.hass.helpers.entity_component.async_update_entity(light) await self.hass.helpers.entity_component.async_update_entity(wlight)
refreshed_state = self.hass.states.get(light) refreshed_state = self.hass.states.get(wlight)
_LOGGER.debug( _LOGGER.debug(
"%s: Current state of %s: %s", "%s: Current state of %s: %s",
switch._name, switch._name,
light, wlight,
refreshed_state, refreshed_state,
) )
changed = compare_to( changed = compare_to(
@ -2010,7 +2219,7 @@ class TurnOnOffListener:
if changed: if changed:
_LOGGER.debug( _LOGGER.debug(
"State of '%s' didn't change wrt 'last_service_data' (context.id=%s)", "State of '%s' didn't change wrt 'last_service_data' (context.id=%s)",
light, wlight,
context.id, context.id,
) )
return True return True
@ -2018,7 +2227,7 @@ class TurnOnOffListener:
"%s: Light '%s' correctly matches our last adapt's service data, continuing..." "%s: Light '%s' correctly matches our last adapt's service data, continuing..."
" context.id=%s.", " context.id=%s.",
switch._name, switch._name,
light, wlight,
context.id, context.id,
) )
return False return False

View file

@ -21,12 +21,15 @@
"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.", "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": { "data": {
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟", "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
"watched_lights": "watched_lights: Use this dictionary of lights to check for manually controlled events in addition to the main lights 🌟 Example: {light.watch_light: light.main_light} will fire manually controlled events to light.main_light",
"prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈", "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`. 📝", "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. ⏲️", "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. 😴", "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": "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. 🌙", "transition_until_sleep": "transition_until_sleep: This option ignores the current state of the sleep switch. When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning color temperature to these values after sunset. 🌙",
"adapt_brightness_until_sleep": "adapt_brightness_until_sleep: Only active when `transition_until_sleep` is true.",
"adapt_color_temp_until_sleep": "adapt_color_temp_until_sleep: Only active when `transition_until_sleep` is true.",
"interval": "interval: Frequency to adapt the lights, in seconds. 🔄", "interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
"min_brightness": "min_brightness: Minimum brightness percentage. 💡", "min_brightness": "min_brightness: Minimum brightness percentage. 💡",
"max_brightness": "max_brightness: Maximum brightness percentage. 💡", "max_brightness": "max_brightness: Maximum brightness percentage. 💡",
@ -43,7 +46,9 @@
"min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇", "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. ⏰", "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`). 🔄", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"flat_limits": "flat_limits: When True, will not calculate between the max/min supported limits of your light. Example: when adapting brightness to 50% while max_brightness is set to 80%, Adaptive Lighting will use 80% instead of 90%",
"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`! 🔒", "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`! 🔒",
"alt_detect_method": "alt_detect_method: alt_detect_method: When true, will check for any significant changes in the opposite direction of where adaptive-lighting tried to adapt last. This is an alternative to 'detect_non_ha_changes' (default: false)",
"detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️", "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. 🔀", "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. ⏲️", "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",

View file

@ -12,6 +12,7 @@ from homeassistant.components.adaptive_lighting.const import (
ADAPT_BRIGHTNESS_SWITCH, ADAPT_BRIGHTNESS_SWITCH,
ADAPT_COLOR_SWITCH, ADAPT_COLOR_SWITCH,
ATTR_TURN_ON_OFF_LISTENER, ATTR_TURN_ON_OFF_LISTENER,
CONF_ALT_DETECT_METHOD,
CONF_AUTORESET_CONTROL, CONF_AUTORESET_CONTROL,
CONF_DETECT_NON_HA_CHANGES, CONF_DETECT_NON_HA_CHANGES,
CONF_INITIAL_TRANSITION, CONF_INITIAL_TRANSITION,
@ -23,9 +24,11 @@ from homeassistant.components.adaptive_lighting.const import (
CONF_SUNRISE_OFFSET, CONF_SUNRISE_OFFSET,
CONF_SUNRISE_TIME, CONF_SUNRISE_TIME,
CONF_SUNSET_TIME, CONF_SUNSET_TIME,
CONF_TAKE_OVER_CONTROL,
CONF_TRANSITION, CONF_TRANSITION,
CONF_TURN_ON_LIGHTS, CONF_TURN_ON_LIGHTS,
CONF_USE_DEFAULTS, CONF_USE_DEFAULTS,
CONF_WHICH_SWITCH,
CONST_COLOR, CONST_COLOR,
DEFAULT_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS,
DEFAULT_NAME, DEFAULT_NAME,
@ -61,7 +64,6 @@ from homeassistant.components.light import (
COLOR_MODE_BRIGHTNESS, COLOR_MODE_BRIGHTNESS,
) )
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.light import SERVICE_TURN_OFF
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
import homeassistant.config as config_util import homeassistant.config as config_util
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
@ -73,6 +75,8 @@ from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PLATFORM, CONF_PLATFORM,
EVENT_STATE_CHANGED, EVENT_STATE_CHANGED,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
@ -221,7 +225,9 @@ async def setup_lights_and_switch(hass, extra_conf=None):
CONF_SUNSET_TIME: datetime.time(SUNSET.hour), CONF_SUNSET_TIME: datetime.time(SUNSET.hour),
CONF_INITIAL_TRANSITION: 0, CONF_INITIAL_TRANSITION: 0,
CONF_TRANSITION: 0, CONF_TRANSITION: 0,
CONF_DETECT_NON_HA_CHANGES: True, CONF_ALT_DETECT_METHOD: False,
CONF_DETECT_NON_HA_CHANGES: False,
CONF_TAKE_OVER_CONTROL: True,
CONF_PREFER_RGB_COLOR: False, CONF_PREFER_RGB_COLOR: False,
CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp
**(extra_conf or {}), **(extra_conf or {}),
@ -748,7 +754,7 @@ async def test_manual_control(hass):
@pytest.mark.dependency(depends=[*GLOBAL_TEST_DEPENDENCIES, "test_manual_control"]) @pytest.mark.dependency(depends=[*GLOBAL_TEST_DEPENDENCIES, "test_manual_control"])
async def test_auto_reset_manual_control(hass): async def test_auto_reset_manual_control(hass):
switch, (light, *_) = await setup_lights_and_switch( switch, (light, *_) = await setup_lights_and_switch(
hass, {CONF_AUTORESET_CONTROL: 0.1} hass, {CONF_AUTORESET_CONTROL: 0.2}
) )
context = switch.create_context("test") # needs to be passed to update method context = switch.create_context("test") # needs to be passed to update method
manual_control = switch.turn_on_off_listener.manual_control manual_control = switch.turn_on_off_listener.manual_control
@ -993,7 +999,7 @@ async def test_state_change_handlers(hass):
# [Config options]: # [Config options]:
transition_used = 2 transition_used = 2
total_events = 5 total_events = 6
async def set_brightness(val: int): async def set_brightness(val: int):
# 'Unsafe' set but we know what we're doing. # 'Unsafe' set but we know what we're doing.
@ -1114,58 +1120,95 @@ async def test_state_change_handlers(hass):
assert listener.transition_timers.get(ENTITY_LIGHT) assert listener.transition_timers.get(ENTITY_LIGHT)
# 5. Execute some checks during a transition # 5. Execute some checks during a transition
_LOGGER.debug("Test detect_non_ha_changes:")
switch._take_over_control = True for i in range(2):
assert switch._take_over_control if i == 0:
switch._detect_non_ha_changes = True _LOGGER.debug("Test detect_non_ha_changes before a transition:")
assert switch._detect_non_ha_changes switch._take_over_control = True
await asyncio.sleep(transition_used / 3) assert switch._take_over_control
# Ensure the timer still exists switch._detect_non_ha_changes = True
timer = listener.transition_timers.get(ENTITY_LIGHT) assert switch._detect_non_ha_changes
assert timer and timer.is_running() switch._alt_detect_method = False
last_service_data = deepcopy(current_service_data) assert not switch._alt_detect_method
await update() elif i == 1:
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] _LOGGER.debug("Test alt_detect_method before a transition:")
await update() switch._take_over_control = True
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] assert switch._take_over_control
timer = listener.transition_timers.get(ENTITY_LIGHT) switch._detect_non_ha_changes = False
assert timer and timer.is_running() assert not switch._detect_non_ha_changes
# Ensure the light did not adapt during the transition. switch._alt_detect_method = True
assert last_service_data == current_service_data assert switch._alt_detect_method
await asyncio.sleep(transition_used / 3)
# Ensure the timer still exists
timer = listener.transition_timers.get(ENTITY_LIGHT)
assert timer and timer.is_running()
last_service_data = deepcopy(current_service_data)
await update()
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
await update()
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
timer = listener.transition_timers.get(ENTITY_LIGHT)
assert timer and timer.is_running()
# Ensure the light did not adapt during the transition.
assert last_service_data == current_service_data
# 6. Assert everything after the transition finishes. # 6. Assert everything after the transition finishes.
await asyncio.sleep(transition_used) await asyncio.sleep(transition_used)
assert listener.last_state_change.get(ENTITY_LIGHT)
assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events
# Timer should be done and reset now.
# This is the assert that I can't fix.
timer = listener.transition_timers.get(ENTITY_LIGHT)
assert not timer or not timer.is_running()
# build last service data for i in range(2):
await update(force=False) if i == 0:
_LOGGER.debug("Test detect_non_ha_changes after a transition:")
switch._take_over_control = True
assert switch._take_over_control
switch._detect_non_ha_changes = True
assert switch._detect_non_ha_changes
switch._alt_detect_method = False
assert not switch._alt_detect_method
elif i == 1:
_LOGGER.debug("Test alt_detect_method after a transition:")
switch._take_over_control = True
assert switch._take_over_control
switch._detect_non_ha_changes = False
assert not switch._detect_non_ha_changes
switch._alt_detect_method = True
assert switch._alt_detect_method
# force=True should not reset manual control. assert listener.last_state_change.get(ENTITY_LIGHT)
await turn_light(True, brightness=40) if i == 1:
await turn_light(True, brightness=20) total_events = 2
await update(force=False) assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] # Timer should be done and reset now.
await update(force=True) timer = listener.transition_timers.get(ENTITY_LIGHT)
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT] assert not timer or not timer.is_running()
# turn light off then on should reset manual control. # build last service data
await turn_light(False) await update(force=False)
await turn_light(True)
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
await turn_light(True, brightness=50) # force=True should not reset manual control.
_LOGGER.debug("Test: Brightness set to %s", 50) 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]
# On next update ENTITY_LIGHT should be marked as manually controlled # turn light off then on should reset manual control.
await update(force=False) await turn_light(False)
assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None await turn_light(True)
assert switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) is not None assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
await turn_light(True, brightness=50)
_LOGGER.debug("Test: Brightness set to %s", 50)
# On next update ENTITY_LIGHT should be marked as manually controlled
await update(force=False)
assert (
switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
)
assert (
switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) is not None
)
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
@pytest.mark.dependency( @pytest.mark.dependency(
@ -1334,10 +1377,89 @@ async def test_area(hass):
assert light.entity_id not in switch.turn_on_off_listener.last_service_data assert light.entity_id not in switch.turn_on_off_listener.last_service_data
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_switch_turn_on_off_toggle(hass):
"""Test adaptive_lighting.change_switch_settings service."""
switch, (_, _, light) = await setup_lights_and_switch(hass)
entity_id = switch.entity_id
assert entity_id not in switch._lights
async def turn_on(which: str, **kwargs):
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: entity_id,
CONF_WHICH_SWITCH: which,
**kwargs,
},
blocking=True,
)
await hass.async_block_till_done()
async def turn_off(which: str, **kwargs):
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: entity_id,
CONF_WHICH_SWITCH: which,
**kwargs,
},
blocking=True,
)
await hass.async_block_till_done()
async def toggle(which: str, **kwargs):
await hass.services.async_call(
DOMAIN,
SERVICE_TOGGLE,
{
ATTR_ENTITY_ID: entity_id,
CONF_WHICH_SWITCH: which,
**kwargs,
},
blocking=True,
)
await hass.async_block_till_done()
# Test sleep
await turn_on("sleep")
assert switch.sleep_mode_switch.is_on
await turn_off("sleep")
assert not switch.sleep_mode_switch.is_on
await toggle("sleep")
assert switch.sleep_mode_switch.is_on
# Test brightness
await turn_on("brightness")
assert switch.adapt_brightness_switch.is_on
await turn_off("brightness")
assert not switch.adapt_brightness_switch.is_on
await toggle("brightness")
assert switch.adapt_brightness_switch.is_on
# Test color
await turn_on("color")
assert switch.adapt_color_switch.is_on
await turn_off("color")
assert not switch.adapt_color_switch.is_on
await toggle("color")
assert switch.adapt_color_switch.is_on
# Test main
await turn_on("main")
assert switch.is_on
await turn_off("main")
assert not switch.is_on
await toggle("main")
assert switch.is_on
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES) @pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_change_switch_settings_service(hass): async def test_change_switch_settings_service(hass):
"""Test adaptive_lighting.change_switch_settings service.""" """Test adaptive_lighting.change_switch_settings service."""
switch, (_, _, light) = await setup_lights_and_switch(hass) switch, (_, _, light) = await setup_lights_and_switch(hass)
switch2, (_, light2, _) = await setup_lights_and_switch(
hass, {CONF_NAME: "second_switch"}
)
entity_id = light.entity_id entity_id = light.entity_id
assert entity_id not in switch._lights assert entity_id not in switch._lights
@ -1346,7 +1468,6 @@ async def test_change_switch_settings_service(hass):
DOMAIN, DOMAIN,
SERVICE_CHANGE_SWITCH_SETTINGS, SERVICE_CHANGE_SWITCH_SETTINGS,
{ {
ATTR_ENTITY_ID: ENTITY_SWITCH,
**kwargs, **kwargs,
}, },
blocking=True, blocking=True,
@ -1355,12 +1476,16 @@ async def test_change_switch_settings_service(hass):
# Test changing sunrise offset # Test changing sunrise offset
assert switch._sun_light_settings.sunrise_offset.total_seconds() == 0 assert switch._sun_light_settings.sunrise_offset.total_seconds() == 0
await change_switch_settings(**{CONF_SUNRISE_OFFSET: 10}) await change_switch_settings(
**{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_SUNRISE_OFFSET: 10}
)
assert switch._sun_light_settings.sunrise_offset.total_seconds() == 10 assert switch._sun_light_settings.sunrise_offset.total_seconds() == 10
# Test changing max brightness # Test changing max brightness
assert switch._sun_light_settings.max_brightness == 100 assert switch._sun_light_settings.max_brightness == 100
await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 50}) await change_switch_settings(
**{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_MAX_BRIGHTNESS: 50}
)
assert switch._sun_light_settings.max_brightness == 50 assert switch._sun_light_settings.max_brightness == 50
# Test changing to illegal max brightness # Test changing to illegal max brightness
@ -1368,20 +1493,33 @@ async def test_change_switch_settings_service(hass):
voluptuous.error.MultipleInvalid, voluptuous.error.MultipleInvalid,
match="value must be at most 100 for dictionary", match="value must be at most 100 for dictionary",
): ):
await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 5000}) await change_switch_settings(
**{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_MAX_BRIGHTNESS: 5000}
)
# Change CONF_MIN_COLOR_TEMP, the factory default is 2000, but setup_lights_and_switch # Change CONF_MIN_COLOR_TEMP, the factory default is 2000, but setup_lights_and_switch
# sets it to 2500 # sets it to 2500
assert switch._sun_light_settings.min_color_temp == 2500 assert switch._sun_light_settings.min_color_temp == 2500
# testing with "factory" should change it to 2000 # testing with "factory" should change it to 2000
await change_switch_settings(**{CONF_USE_DEFAULTS: "factory"}) await change_switch_settings(
**{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_USE_DEFAULTS: "factory"}
)
assert switch._sun_light_settings.min_color_temp == 2000 assert switch._sun_light_settings.min_color_temp == 2000
# testing with "current" should not change things # testing with "current" should not change things
await change_switch_settings(**{CONF_USE_DEFAULTS: "current"}) await change_switch_settings(
**{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_USE_DEFAULTS: "current"}
)
assert switch._sun_light_settings.min_color_temp == 2000 assert switch._sun_light_settings.min_color_temp == 2000
# testing with "configuration" should revert back to 2500 # testing with "configuration" should revert back to 2500
await change_switch_settings(**{CONF_USE_DEFAULTS: "configuration"}) await change_switch_settings(
**{ATTR_ENTITY_ID: ENTITY_SWITCH, CONF_USE_DEFAULTS: "configuration"}
)
assert switch._sun_light_settings.min_color_temp == 2500 assert switch._sun_light_settings.min_color_temp == 2500
# testing with no switches or lights defined.
assert switch2._sun_light_settings.max_brightness == 100
await change_switch_settings(**{CONF_MAX_BRIGHTNESS: 50})
assert switch2._sun_light_settings.max_brightness == 50