From cdc3585a66969211a08762eb05ae36ab3f070130 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Mon, 3 Apr 2023 07:58:20 +0000
Subject: [PATCH 001/549] docs: add igiannakas as a contributor for code (#517)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 063b429c..344b3112 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -429,6 +429,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "igiannakas",
+ "name": "igiannakas",
+ "avatar_url": "https://avatars.githubusercontent.com/u/59056762?v=4",
+ "profile": "https://github.com/igiannakas",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index c354153d..9ebc7397 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -436,6 +436,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting
 Skyler Carlson 📖 |
 Chris 💻 |
 Raman Gupta 💻 |
+  igiannakas 💻 |
From 19fcb1d6b9c1c0a0abcb0a88bc16b05839818726 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 3 Apr 2023 00:58:39 -0700
Subject: [PATCH 002/549] Automatically generate more service tables in the
README (#509)
* Automatically sync more data
* Automatically generate apply table
* Generate manual control
* Add manual control docs
* Update README.md
* Move docs functions to separate file
* Update code
* simple
* no path
* link paths
* Fix link
* missed
* cd core
* Update README.md
* Allow alternative docs
* update readme
* More special
* Fx
* Update README.md
* Remove common descriptions
* Update README.md
* Update README.md
* Rephrase
* Update README.md
---------
Co-authored-by: github-actions[bot]
---
.github/workflows/pytest.yaml | 8 +-
.github/workflows/update-readme.yml | 7 +-
README.md | 75 ++++----
.../adaptive_lighting/_docs_helpers.py | 116 ++++++++++++
custom_components/adaptive_lighting/const.py | 142 +++++++--------
.../adaptive_lighting/services.yaml | 168 +++++++++---------
custom_components/adaptive_lighting/switch.py | 28 +--
7 files changed, 333 insertions(+), 211 deletions(-)
create mode 100644 custom_components/adaptive_lighting/_docs_helpers.py
diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml
index 47d4df8a..02bf93e3 100644
--- a/.github/workflows/pytest.yaml
+++ b/.github/workflows/pytest.yaml
@@ -31,8 +31,8 @@ jobs:
echo "::notice::### 4. ERROR:homeassistant.setup:Setup failed for 'component': Unable to import component: No module named ''module'' ###"
echo "::notice::### 5. add 'component'.'module' (without the '') from the above log into the 'required' list inside of 'test_dependencies.py' ###"
echo "::notice::### 6. Try again! If more issues persist they should be easily solvable by reading the verbose logs now. ###"
- - name: Run pytest
- timeout-minutes: 60
+
+ - name: Link custom_components/adaptive_lighting
run: |
cd core
@@ -46,6 +46,10 @@ jobs:
ln -fs ../../../tests adaptive_lighting
cd -
+ - name: Run pytest
+ timeout-minutes: 60
+ run: |
+ cd core
python3 -X dev -m pytest \
-qq \
--timeout=9 \
diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml
index 4d4c4530..bc31bb94 100644
--- a/.github/workflows/update-readme.yml
+++ b/.github/workflows/update-readme.yml
@@ -22,10 +22,15 @@ jobs:
with:
python_version: "3.10"
- - name: Install pandas and tabulate
+ - name: Install markdown-code-runner and README code dependencies
run: |
pip install markdown-code-runner pandas tabulate
+ - name: Link custom_components/adaptive_lighting
+ run: |
+ cd core/homeassistant/components
+ ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting
+
- name: Run markdown-code-runner
run: markdown-code-runner --debug README.md
diff --git a/README.md b/README.md
index 9ebc7397..5b98de8b 100644
--- a/README.md
+++ b/README.md
@@ -82,11 +82,8 @@ All of the configuration options are listed below, along with their default valu
The YAML and frontend configuration methods support all of the options listed below.
-
-
-
-
-
+
+
@@ -94,27 +91,27 @@ The YAML and frontend configuration methods support all of the options listed be
| Variable name | Description | Default | Type |
|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
| `lights` | List of light entities to be controlled by Adaptive Lighting (may be empty). 🌟 | `[]` | list of `entity_id`s |
-| `prefer_rgb_color` | Use RGB color adjustment instead of native light color temperature. 🌈 | `False` | `bool` |
+| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
-| `adapt_until_sleep` | When `true`, Adaptive Lighting will use the sleep settings as the minimum, and transition to these values past the sunset | `False` | `bool` |
-| `initial_transition` | Duration of the first transition when lights turn from `off` to `on`. ⏲️ | `1` | `float` 0-6553 |
-| `sleep_transition` | Duration of transition when 'sleep mode' is toggled. 😴 | `1` | `float` 0-6553 |
+| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
+| `sleep_transition` | Duration of transition when 'sleep mode' is toggled in seconds. 😴 | `1` | `float` 0-6553 |
| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
+| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
-| `sleep_brightness` | Brightness of lights in sleep mode. 😴 | `1` | `int` 1-100 |
+| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
| `sleep_rgb_or_color_temp` | Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
-| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`). 😴 | `1000` | `int` 1000-10000 |
+| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). 🌈 | `[255, 56, 0]` | RGB color |
-| `sunrise_time` | Set a fixed time for sunrise. 🌅 | `None` | `str` |
-| `max_sunrise_time` | Set the latest virtual sunrise time, allowing for earlier real sunrises. 🌅 | `None` | `str` |
-| `sunrise_offset` | Adjust sunrise time with a positive or negative offset. ⏰ | `0` | `int` |
-| `sunset_time` | Set a fixed time for sunset. 🌇 | `None` | `str` |
-| `min_sunset_time` | Set the earliest virtual sunset time, allowing for later real sunsets. 🌇 | `None` | `str` |
-| `sunset_offset` | Adjust sunset time with a positive or negative offset. ⏰ | `0` | `int` |
+| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
+| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
+| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
+| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
+| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
@@ -158,26 +155,42 @@ adaptive_lighting:
`adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand.
-| Service data attribute | Required | Description |
-| ---------------------- | -------- | -------------------------------------------------------------------------------------------- |
-| `entity_id` | ✅ | The `entity_id` of the switch with the settings to apply. |
-| `lights` | ❌ | A light (or list of lights) to apply the settings to. |
-| `transition` | ❌ | The number of seconds for the transition. |
-| `adapt_brightness` | ❌ | Whether to change the brightness of the light or not. |
-| `adapt_color` | ❌ | Whether to adapt the color on supporting lights. |
-| `prefer_rgb_color` | ❌ | Whether to prefer RGB color adjustment over of native light color temperature when possible. |
-| `turn_on_lights` | ❌ | Whether to turn on lights that are currently off. |
+
+
+
+
+
+
+| Service data attribute | Description | Required | Type |
+|:-------------------------|:-------------------------------------------------------------------------------------|:-----------|:---------------------|
+| `entity_id` | The `entity_id` of the switch with the settings to apply. 📝 | ✅ | list of `entity_id`s |
+| `lights` | A light (or list of lights) to apply the settings to. 💡 | ❌ | list of `entity_id`s |
+| `transition` | Duration of transition when lights change, in seconds. 🕑 | ❌ | `float` 0-6553 |
+| `adapt_brightness` | Whether to adapt the brightness of the light. 🌞 | ❌ | bool |
+| `adapt_color` | Whether to adapt the color on supporting lights. 🌈 | ❌ | bool |
+| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | ❌ | bool |
+| `turn_on_lights` | Whether to turn on lights that are currently off. 🔆 | ❌ | bool |
+
+
#### `adaptive_lighting.set_manual_control`
`adaptive_lighting.set_manual_control` can mark (or unmark) whether a light is "manually controlled", meaning that when a light has `manual_control`, the light is not adapted.
-| Service data attribute | Required | Description |
-| ---------------------- | -------- | --------------------------------------------------------------------------------------------------- |
-| `entity_id` | ✅ | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". |
-| `lights` | ❌ | entity_id(s) of lights, if not specified, all lights in the switch are selected. |
-| `manual_control` | ❌ | Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true |
+
+
+
+
+
+
+| Service data attribute | Description | Required | Type |
+|:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------|
+| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s |
+| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s |
+| `manual_control` | Whether to add ('true') or remove ('false') the light from the 'manual_control' list. 🔒 | ❌ | bool |
+
+
#### `adaptive_lighting.change_switch_settings`
`adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation.
diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py
new file mode 100644
index 00000000..40afc235
--- /dev/null
+++ b/custom_components/adaptive_lighting/_docs_helpers.py
@@ -0,0 +1,116 @@
+from typing import Any
+
+from homeassistant.helpers import selector
+import homeassistant.helpers.config_validation as cv
+import pandas as pd
+import voluptuous as vol
+
+from .const import (
+ DOCS,
+ DOCS_APPLY,
+ DOCS_MANUAL_CONTROL,
+ SET_MANUAL_CONTROL_SCHEMA,
+ VALIDATION_TUPLES,
+ apply_service_schema,
+)
+
+
+def _format_voluptuous_instance(instance):
+ coerce_type = None
+ min_val = None
+ max_val = None
+
+ for validator in instance.validators:
+ if isinstance(validator, vol.Coerce):
+ coerce_type = validator.type.__name__
+ elif isinstance(validator, (vol.Clamp, vol.Range)):
+ min_val = validator.min
+ max_val = validator.max
+
+ if min_val is not None and max_val is not None:
+ return f"`{coerce_type}` {min_val}-{max_val}"
+ elif min_val is not None:
+ return f"`{coerce_type} > {min_val}`"
+ elif max_val is not None:
+ return f"`{coerce_type} < {max_val}`"
+ else:
+ return f"`{coerce_type}`"
+
+
+def _type_to_str(type_: Any) -> str:
+ """Convert a (voluptuous) type to a string."""
+ if type_ == cv.entity_ids:
+ return "list of `entity_id`s"
+ elif type_ in (bool, int, float, str):
+ return f"`{type_.__name__}`"
+ elif type_ == cv.boolean:
+ return "bool"
+ elif isinstance(type_, vol.All):
+ return _format_voluptuous_instance(type_)
+ elif isinstance(type_, vol.In):
+ return f"one of `{type_.container}`"
+ elif isinstance(type_, selector.SelectSelector):
+ return f"one of `{type_.config['options']}`"
+ elif isinstance(type_, selector.ColorRGBSelector):
+ return "RGB color"
+ else:
+ raise ValueError(f"Unknown type: {type_}")
+
+
+def generate_config_markdown_table():
+ import pandas as pd
+
+ rows = []
+ for k, default, type_ in VALIDATION_TUPLES:
+ description = DOCS[k]
+ row = {
+ "Variable name": f"`{k}`",
+ "Description": description,
+ "Default": f"`{default}`",
+ "Type": _type_to_str(type_),
+ }
+ rows.append(row)
+
+ df = pd.DataFrame(rows)
+ return df.to_markdown(index=False)
+
+
+def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]:
+ result = {}
+ for key, value in schema.schema.items():
+ if isinstance(key, vol.Optional):
+ default_value = key.default
+ result[key.schema] = (default_value, value)
+ return result
+
+
+def _generate_service_markdown_table(
+ schema: dict[str, tuple[Any, Any]], alternative_docs: dict[str, str] = None
+):
+ schema = _schema_to_dict(schema)
+ rows = []
+ for k, (default, type_) in schema.items():
+ if alternative_docs is not None and k in alternative_docs:
+ description = alternative_docs[k]
+ else:
+ description = DOCS[k]
+ row = {
+ "Service data attribute": f"`{k}`",
+ "Description": description,
+ "Required": "✅" if default == vol.UNDEFINED else "❌",
+ "Type": _type_to_str(type_),
+ }
+ rows.append(row)
+
+ df = pd.DataFrame(rows)
+ return df.to_markdown(index=False)
+
+
+def generate_apply_markdown_table():
+ return _generate_service_markdown_table(apply_service_schema(), DOCS_APPLY)
+
+
+def generate_set_manual_control_markdown_table():
+ return _generate_service_markdown_table(
+ SET_MANUAL_CONTROL_SCHEMA, DOCS_MANUAL_CONTROL
+ )
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index a5a5f2c1..bb0c07cd 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -1,6 +1,7 @@
"""Constants for the Adaptive Lighting integration."""
from homeassistant.components.light import VALID_TRANSITION
+from homeassistant.const import CONF_ENTITY_ID
from homeassistant.helpers import selector
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
@@ -14,7 +15,7 @@ DOMAIN = "adaptive_lighting"
SUN_EVENT_NOON = "solar_noon"
SUN_EVENT_MIDNIGHT = "solar_midnight"
-DOCS = {}
+DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"}
CONF_NAME, DEFAULT_NAME = "name", "default"
@@ -45,11 +46,14 @@ DOCS[CONF_INCLUDE_CONFIG_IN_ATTRIBUTES] = (
CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1
DOCS[CONF_INITIAL_TRANSITION] = (
- "Duration of the first transition when lights turn " "from `off` to `on`. ⏲️"
+ "Duration of the first transition when lights turn "
+ "from `off` to `on` in seconds. ⏲️"
)
CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1
-DOCS[CONF_SLEEP_TRANSITION] = "Duration of transition when 'sleep mode' is toggled. 😴"
+DOCS[CONF_SLEEP_TRANSITION] = (
+ "Duration of transition when 'sleep mode' is toggled " "in seconds. 😴"
+)
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90
DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄"
@@ -73,9 +77,10 @@ DOCS[CONF_ONLY_ONCE] = (
)
CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False
-DOCS[
- CONF_PREFER_RGB_COLOR
-] = "Use RGB color adjustment instead of native light color temperature. 🌈"
+DOCS[CONF_PREFER_RGB_COLOR] = (
+ "Whether to prefer RGB color adjustment over "
+ "light color temperature when possible. 🌈"
+)
CONF_SEPARATE_TURN_ON_COMMANDS, DEFAULT_SEPARATE_TURN_ON_COMMANDS = (
"separate_turn_on_commands",
@@ -87,12 +92,12 @@ DOCS[CONF_SEPARATE_TURN_ON_COMMANDS] = (
)
CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS = "sleep_brightness", 1
-DOCS[CONF_SLEEP_BRIGHTNESS] = "Brightness of lights in sleep mode. 😴"
+DOCS[CONF_SLEEP_BRIGHTNESS] = "Brightness percentage of lights in sleep mode. 😴"
CONF_SLEEP_COLOR_TEMP, DEFAULT_SLEEP_COLOR_TEMP = "sleep_color_temp", 1000
DOCS[CONF_SLEEP_COLOR_TEMP] = (
"Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is "
- "`color_temp`). 😴"
+ "`color_temp`) in Kelvin. 😴"
)
CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0]
@@ -104,30 +109,36 @@ CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = (
"sleep_rgb_or_color_temp",
"color_temp",
)
-DOCS[
- CONF_SLEEP_RGB_OR_COLOR_TEMP
-] = "Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙"
+DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = (
+ "Use either `'rgb_color'` or `'color_temp'` " "in sleep mode. 🌙"
+)
CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0
-DOCS[CONF_SUNRISE_OFFSET] = "Adjust sunrise time with a positive or negative offset. ⏰"
+DOCS[CONF_SUNRISE_OFFSET] = (
+ "Adjust sunrise time with a positive or negative offset " "in seconds. ⏰"
+)
CONF_SUNRISE_TIME = "sunrise_time"
-DOCS[CONF_SUNRISE_TIME] = "Set a fixed time for sunrise. 🌅"
+DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅"
CONF_MAX_SUNRISE_TIME = "max_sunrise_time"
DOCS[CONF_MAX_SUNRISE_TIME] = (
- "Set the latest virtual sunrise time, allowing" " for earlier real sunrises. 🌅"
+ "Set the latest virtual sunrise time (HH:MM:SS), allowing"
+ " for earlier real sunrises. 🌅"
)
CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0
-DOCS[CONF_SUNSET_OFFSET] = "Adjust sunset time with a positive or negative offset. ⏰"
+DOCS[
+ CONF_SUNSET_OFFSET
+] = "Adjust sunset time with a positive or negative offset in seconds. ⏰"
CONF_SUNSET_TIME = "sunset_time"
-DOCS[CONF_SUNSET_TIME] = "Set a fixed time for sunset. 🌇"
+DOCS[CONF_SUNSET_TIME] = "Set a fixed time (HH:MM:SS) for sunset. 🌇"
CONF_MIN_SUNSET_TIME = "min_sunset_time"
DOCS[CONF_MIN_SUNSET_TIME] = (
- "Set the earliest virtual sunset time, allowing" " for later real sunsets. 🌇"
+ "Set the earliest virtual sunset time (HH:MM:SS), allowing"
+ " for later real sunsets. 🌇"
)
CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True
@@ -145,8 +156,8 @@ CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP = (
False,
)
DOCS[CONF_ADAPT_UNTIL_SLEEP] = (
- "When checked, Adaptive Lighting will use the sleep settings as the minimum,"
- " and transition to these values past the sunset"
+ "When enabled, Adaptive Lighting will treat sleep settings as the minimum, "
+ "transitioning to these values after sunset. 🌙"
)
CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0
@@ -174,18 +185,36 @@ ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener"
UNDO_UPDATE_LISTENER = "undo_update_listener"
NONE_STR = "None"
ATTR_ADAPT_COLOR = "adapt_color"
+DOCS[ATTR_ADAPT_COLOR] = "Whether to adapt the color on supporting lights. 🌈"
ATTR_ADAPT_BRIGHTNESS = "adapt_brightness"
+DOCS[ATTR_ADAPT_BRIGHTNESS] = "Whether to adapt the brightness of the light. 🌞"
SERVICE_SET_MANUAL_CONTROL = "set_manual_control"
CONF_MANUAL_CONTROL = "manual_control"
+DOCS[CONF_MANUAL_CONTROL] = "Whether to manually control the lights. 🔒"
SERVICE_APPLY = "apply"
CONF_TURN_ON_LIGHTS = "turn_on_lights"
+DOCS[CONF_TURN_ON_LIGHTS] = "Whether to turn on lights that are currently off. 🔆"
SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings"
CONF_USE_DEFAULTS = "use_defaults"
-
+DOCS[CONF_USE_DEFAULTS] = "Whether to use default settings for the switches. ⚙️"
TURNING_OFF_DELAY = 5
+DOCS_MANUAL_CONTROL = {
+ CONF_ENTITY_ID: "The `entity_id` of the switch in which to (un)mark the "
+ "light as being `manually controlled`. 📝",
+ CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the "
+ "switch are selected. 💡",
+ CONF_MANUAL_CONTROL: "Whether to add ('true') or remove ('false') the "
+ "light from the 'manual_control' list. 🔒",
+}
+
+DOCS_APPLY = {
+ CONF_ENTITY_ID: "The `entity_id` of the switch with the settings to apply. 📝",
+ CONF_LIGHTS: "A light (or list of lights) to apply the settings to. 💡",
+}
+
def int_between(min_int, max_int):
"""Return an integer between 'min_int' and 'max_int'."""
@@ -290,55 +319,28 @@ _DOMAIN_SCHEMA = vol.Schema(
)
-def _format_voluptuous_instance(instance):
- coerce_type = None
- min_val = None
- max_val = None
-
- for validator in instance.validators:
- if isinstance(validator, vol.Coerce):
- coerce_type = validator.type.__name__
- elif isinstance(validator, (vol.Clamp, vol.Range)):
- min_val = validator.min
- max_val = validator.max
-
- if min_val is not None and max_val is not None:
- return f"`{coerce_type}` {min_val}-{max_val}"
- elif min_val is not None:
- return f"`{coerce_type} > {min_val}`"
- elif max_val is not None:
- return f"`{coerce_type} < {max_val}`"
- else:
- return f"`{coerce_type}`"
-
-
-def generate_markdown_table():
- import pandas as pd
-
- rows = []
- for k, default, type_ in VALIDATION_TUPLES:
- description = DOCS[k]
- if type_ == cv.entity_ids:
- type_ = "list of `entity_id`s"
- elif type_ in (bool, int, float, str):
- type_ = f"`{type_.__name__}`"
- elif isinstance(type_, vol.All):
- type_ = _format_voluptuous_instance(type_)
- elif isinstance(type_, vol.In):
- type_ = f"one of `{type_.container}`"
- elif isinstance(type_, selector.SelectSelector):
- type_ = f"one of `{type_.config['options']}`"
- elif isinstance(type_, selector.ColorRGBSelector):
- type_ = "RGB color"
- else:
- raise ValueError(f"Unknown type: {type_}")
- row = {
- "Variable name": f"`{k}`",
- "Description": description,
- "Default": f"`{default}`",
- "Type": type_,
+def apply_service_schema(initial_transition: int = 1):
+ """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_TRANSITION,
+ default=initial_transition,
+ ): 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,
}
- rows.append(row)
+ )
- df = pd.DataFrame(rows)
- return df.to_markdown(index=False)
+
+SET_MANUAL_CONTROL_SCHEMA = vol.Schema(
+ {
+ vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
+ vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
+ vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean,
+ }
+)
diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml
index 373e796a..89c112e3 100755
--- a/custom_components/adaptive_lighting/services.yaml
+++ b/custom_components/adaptive_lighting/services.yaml
@@ -2,7 +2,7 @@ apply:
description: Applies the current Adaptive Lighting settings to lights.
fields:
entity_id:
- description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used.
+ description: "Entity ID of the switch. \U0001F4DD"
example: switch.adaptive_lighting_default
selector:
entity:
@@ -10,43 +10,42 @@ apply:
domain: switch
multiple: false
lights:
- description: entity_id(s) of lights, if not specified, all lights in the switch are selected.
+ description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F"
example: light.bedroom_ceiling
selector:
entity:
domain: light
multiple: true
transition:
- description: Transition of the lights.
+ description: "Duration of transition when lights change, in seconds. \U0001F551"
example: 10
selector:
- text:
+ text: null
adapt_brightness:
- description: "Adapt the 'brightness', default: true"
+ description: "Whether to adapt the brightness of the light. \U0001F31E"
example: true
selector:
- boolean:
+ boolean: null
adapt_color:
- description: "Adapt the color_temp/color_rgb, default: true"
+ description: "Whether to adapt the color of the light. \U0001F308"
example: true
selector:
- boolean:
+ boolean: null
prefer_rgb_color:
- description: "Prefer to use color_rgb over color_temp if possible, default: false"
+ description: "Use RGB color adjustment instead of native light color temperature. \U0001F308"
example: false
selector:
- boolean:
+ boolean: null
turn_on_lights:
- description: "Turn on the lights that are off, default: false"
+ description: "Whether to turn on lights if they are off. \U0001F506"
example: false
selector:
- boolean:
-
+ boolean: null
set_manual_control:
description: Mark whether a light is 'manually controlled'.
fields:
entity_id:
- description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used.
+ description: "Entity ID of the switch. \U0001F4DD"
example: switch.adaptive_lighting_default
selector:
entity:
@@ -54,138 +53,137 @@ set_manual_control:
domain: switch
multiple: false
lights:
- description: entity_id(s) of lights, if not specified, all lights in the switch are selected.
+ description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F"
example: light.bedroom_ceiling
selector:
entity:
domain: light
multiple: true
manual_control:
- description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true"
+ description: "Whether to manually control the lights. \U0001F512"
example: true
default: true
selector:
- boolean:
-
+ boolean: null
change_switch_settings:
- description: "Change any settings you'd like in the switch. All options here are the same as in the config flow."
+ description: Change any settings you'd like in the switch. All options here are the same as in the config flow.
fields:
entity_id:
- description: "entity_id of the Adaptive Lighting switch."
+ description: "Entity ID of the switch. \U0001F4DD"
required: true
selector:
entity:
domain: switch
use_defaults:
- description: "(default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation."
- example: "current"
+ description: "Whether to use default settings for the switches. \u2699\uFE0F"
+ example: current
required: false
- default: "current"
+ default: current
selector:
select:
options:
- - "current"
- - "configuration"
- - "factory"
+ - current
+ - configuration
+ - factory
include_config_in_attributes:
- description: "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)"
+ description: "Show all options as attributes on the switch in Home Assistant when set to `true`. \U0001F4DD"
required: false
selector:
- boolean:
+ boolean: null
turn_on_lights:
- description: "Turn on the lights that are off, default: false"
+ description: "Whether to turn on lights if they are off. \U0001F506"
example: false
required: false
selector:
- boolean:
+ boolean: null
initial_transition:
- description: "initial_transition: When lights turn 'off' to 'on'. (seconds)"
+ description: "Duration of the first transition when lights turn from `off` to `on` in seconds. \u23F2\uFE0F"
example: 1
required: false
selector:
- text:
+ text: null
sleep_transition:
- description: "sleep_transition: When 'sleep_state' changes. (seconds)"
+ description: "Duration of transition when 'sleep mode' is toggled in seconds. \U0001F634"
example: 1
required: false
selector:
- text:
+ text: null
max_brightness:
- description: "max_brightness: Highest brightness of lights during a cycle. (%)"
+ description: "Maximum brightness percentage. \U0001F4A1"
required: false
example: 100
selector:
- text:
+ text: null
max_color_temp:
- description: "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)"
+ description: "Coldest color temperature in Kelvin. \u2744\uFE0F"
required: false
example: 5500
selector:
- text:
+ text: null
min_brightness:
- description: "min_brightness: Lowest brightness of lights during a cycle. (%)"
+ description: "Minimum brightness percentage. \U0001F4A1"
required: false
example: 1
selector:
- text:
+ text: null
min_color_temp:
- description: "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)"
+ description: "Warmest color temperature in Kelvin. \U0001F525"
required: false
example: 2000
selector:
- text:
+ text: null
only_once:
- description: "only_once: Only adapt the lights when turning them on."
+ description: "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). \U0001F504"
example: false
required: false
selector:
- boolean:
+ boolean: null
prefer_rgb_color:
- description: "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible."
+ description: "Use RGB color adjustment instead of native light color temperature. \U0001F308"
required: false
example: false
selector:
- boolean:
+ boolean: null
separate_turn_on_commands:
- description: "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights)."
+ description: "Use separate `light.turn_on` calls for color and brightness, needed for some light types. \U0001F500"
required: false
example: false
selector:
- boolean:
+ boolean: null
send_split_delay:
- description: "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly."
+ description: "Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. \u23F2\uFE0F"
required: false
example: 0
selector:
- boolean:
+ boolean: null
sleep_brightness:
- description: "sleep_brightness, Brightness setting for Sleep Mode. (%)"
+ description: "Brightness percentage of lights in sleep mode. \U0001F634"
required: false
example: 1
selector:
- text:
+ text: null
sleep_rgb_or_color_temp:
- description: "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'"
+ description: "Use either `'rgb_color'` or `'color_temp'` in sleep mode. \U0001F319"
required: false
- example: "color_temp"
+ example: color_temp
selector:
select:
options:
- - "rgb_color"
- - "color_temp"
+ - rgb_color
+ - color_temp
sleep_rgb_color:
- description: "sleep_rgb_color, in RGB"
+ description: "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). \U0001F308"
required: false
selector:
- color_rgb:
+ color_rgb: null
sleep_color_temp:
- description: "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)"
+ description: "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. \U0001F634"
required: false
example: 1000
selector:
- text:
+ text: null
sunrise_offset:
- description: sunrise_offset, in +/- seconds (integer)
+ description: "Adjust sunrise time with a positive or negative offset in seconds. \u23F0"
required: false
example: 0
selector:
@@ -193,64 +191,64 @@ change_switch_settings:
min: 0
max: 86300
sunrise_time:
- description: sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)
+ description: "Set a fixed time (HH:MM:SS) for sunrise. \U0001F305"
required: false
- example: ""
+ example: ''
selector:
- time:
+ time: null
sunset_offset:
- description: sunset_offset, in +/- seconds (integer)
+ description: "Adjust sunset time with a positive or negative offset in seconds. \u23F0"
required: false
- example: ""
+ example: ''
selector:
number:
min: 0
max: 86300
sunset_time:
- description: sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)
- example: ""
+ description: "Set a fixed time (HH:MM:SS) for sunset. \U0001F307"
+ example: ''
required: false
selector:
- time:
+ time: null
max_sunrise_time:
- description: "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)"
- example: ""
+ description: "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. \U0001F305"
+ example: ''
required: false
selector:
- time:
+ time: null
min_sunset_time:
- description: "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)"
- example: ""
+ description: "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. \U0001F307"
+ example: ''
required: false
selector:
- time:
+ time: null
take_over_control:
- description: "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on."
+ description: "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! \U0001F512"
required: false
example: true
selector:
- boolean:
+ boolean: null
detect_non_ha_changes:
- description: "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)"
+ description: "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. \U0001F575\uFE0F"
required: false
example: false
selector:
- boolean:
+ boolean: null
transition:
- description: "Transition time when applying a change to the lights (seconds)"
+ description: "Duration of transition when lights change, in seconds. \U0001F551"
required: false
example: 45
selector:
- text:
+ text: null
adapt_delay:
- description: "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering."
+ description: "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. \u23F2\uFE0F"
required: false
example: 0
selector:
- text:
+ text: null
autoreset_control_seconds:
- description: "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)"
+ description: "Automatically reset the manual control after a number of seconds. Set to 0 to disable. \u23F2\uFE0F"
required: false
example: 0
selector:
- text:
+ text: null
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 75361965..3e1c86db 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -39,7 +39,6 @@ from homeassistant.components.light import (
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
SUPPORT_TRANSITION,
- VALID_TRANSITION,
is_on,
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
@@ -137,11 +136,13 @@ from .const import (
SERVICE_APPLY,
SERVICE_CHANGE_SWITCH_SETTINGS,
SERVICE_SET_MANUAL_CONTROL,
+ SET_MANUAL_CONTROL_SCHEMA,
SLEEP_MODE_SWITCH,
SUN_EVENT_MIDNIGHT,
SUN_EVENT_NOON,
TURNING_OFF_DELAY,
VALIDATION_TUPLES,
+ apply_service_schema,
replace_none_str,
)
@@ -495,20 +496,9 @@ async def async_setup_entry(
domain=DOMAIN,
service=SERVICE_APPLY,
service_func=handle_apply,
- schema=vol.Schema(
- {
- vol.Optional("entity_id"): cv.entity_ids,
- vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
- vol.Optional(
- CONF_TRANSITION,
- default=switch._initial_transition, # pylint: disable=protected-access
- ): 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=apply_service_schema(
+ switch._initial_transition
+ ), # pylint: disable=protected-access
)
# Register `set_manual_control` service
@@ -516,13 +506,7 @@ async def async_setup_entry(
domain=DOMAIN,
service=SERVICE_SET_MANUAL_CONTROL,
service_func=handle_set_manual_control,
- schema=vol.Schema(
- {
- vol.Optional("entity_id"): cv.entity_ids,
- vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
- vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean,
- }
- ),
+ schema=SET_MANUAL_CONTROL_SCHEMA,
)
args = {vol.Optional(CONF_USE_DEFAULTS, default="current"): cv.string}
From 87391e6d24752141b1559ff65c95478b405ef185 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 3 Apr 2023 01:01:58 -0700
Subject: [PATCH 003/549] Bump to 1.9.0 (#518)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 39276752..803069a7 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": [],
- "version": "1.8.0"
+ "version": "1.9.0"
}
From c6a6cd323f701f356decbcca4a75a03500ca7f6b Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Mon, 3 Apr 2023 03:02:06 -0500
Subject: [PATCH 004/549] Basic community fixes PR (#460)
* Fixes #423
#423
* Do not adapt lights turned on with custom payloads.
* Update switch.py
* Issue fixes
#423, #378, #403, #449
* quickly test #274
* Revert feature requests, this branch only has fixes.
Reverted FR 274
* pre-commit fix
* Create automerge.yaml
* test
* Delete automerge.yaml
My bad.
* Fix #460 and #408
* see @basnijholt 's comment in #450.
* @basnijholt requested changes.
---------
Co-authored-by: Bas Nijholt
---
.../adaptive_lighting/services.yaml | 4 ----
custom_components/adaptive_lighting/switch.py | 21 ++++++++++++++++---
tests/test_switch.py | 11 ++++++++--
3 files changed, 27 insertions(+), 9 deletions(-)
mode change 100755 => 100644 custom_components/adaptive_lighting/services.yaml
diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml
old mode 100755
new mode 100644
index 89c112e3..351fe5b5
--- a/custom_components/adaptive_lighting/services.yaml
+++ b/custom_components/adaptive_lighting/services.yaml
@@ -3,7 +3,6 @@ apply:
fields:
entity_id:
description: "Entity ID of the switch. \U0001F4DD"
- example: switch.adaptive_lighting_default
selector:
entity:
integration: adaptive_lighting
@@ -11,7 +10,6 @@ apply:
multiple: false
lights:
description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F"
- example: light.bedroom_ceiling
selector:
entity:
domain: light
@@ -46,7 +44,6 @@ set_manual_control:
fields:
entity_id:
description: "Entity ID of the switch. \U0001F4DD"
- example: switch.adaptive_lighting_default
selector:
entity:
integration: adaptive_lighting
@@ -54,7 +51,6 @@ set_manual_control:
multiple: false
lights:
description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F"
- example: light.bedroom_ceiling
selector:
entity:
domain: light
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 3e1c86db..4b9e1856 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -582,7 +582,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
def _supported_features(hass: HomeAssistant, light: str):
state = hass.states.get(light)
- supported_features = state.attributes[ATTR_SUPPORTED_FEATURES]
+ supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
supported = {
key for key, value in _SUPPORT_OPTS.items() if supported_features & value
}
@@ -1017,7 +1017,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if prefer_rgb_color is None:
prefer_rgb_color = self._prefer_rgb_color
- if "transition" in features:
+ # Check transition == 0 to fix #378
+ if "transition" in features and transition > 0:
service_data[ATTR_TRANSITION] = transition
# The switch might be off and not have _settings set.
@@ -1064,7 +1065,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
):
return
- self.turn_on_off_listener.last_service_data[light] = service_data
+ # See #80. Doesn't check if transitions differ but it does the job.
+ last_service_data = self.turn_on_off_listener.last_service_data
+ if light in last_service_data and last_service_data[light] == service_data:
+ _LOGGER.debug(
+ "%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')",
+ self._name,
+ light,
+ context.id,
+ )
+ return
+ else:
+ self.turn_on_off_listener.last_service_data[light] = service_data
async def turn_on(service_data):
_LOGGER.debug(
@@ -1489,11 +1501,14 @@ class SunLightSettings:
rgb_color: tuple[float, float, float] = color_temperature_to_rgb(
color_temp_kelvin
)
+ # backwards compatibility for versions < 1.3.1 - see #403
+ color_temp_mired: float = math.floor(1000000 / color_temp_kelvin)
xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color)
hs_color: tuple[float, float] = color_xy_to_hs(*xy_color)
return {
"brightness_pct": brightness_pct,
"color_temp_kelvin": color_temp_kelvin,
+ "color_temp_mired": color_temp_mired,
"rgb_color": rgb_color,
"xy_color": xy_color,
"hs_color": hs_color,
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 5b74c94c..212cea82 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -527,11 +527,18 @@ async def test_manual_control(hass):
await turn_switch(True, entity_id)
assert not manual_control[ENTITY_LIGHT]
+ # Check that manual control is still enabled if set while bulb is off.
+ # Test issue #37
+ await turn_light(False)
+ await change_manual_control(True)
+ await turn_light(True)
+ assert manual_control[ENTITY_LIGHT]
+
# Check that when 'adapt_brightness' is off, changing the brightness
# doesn't mark it as manually controlled but changing color_temp
# does
- await turn_light(False) # reset manually controlled status
- await turn_light(True)
+ await turn_light(False)
+ await turn_light(True) # reset manually controlled status
assert not manual_control[ENTITY_LIGHT]
await switch.adapt_brightness_switch.async_turn_off()
await turn_light(True, brightness=increased_brightness())
From 0958feb744d503c2a7680476e9c80dcdc44d0b75 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 3 Apr 2023 01:47:01 -0700
Subject: [PATCH 005/549] Undo accidental changes introduced in #509, but adds
the changes from #460 (#521)
---
.../adaptive_lighting/services.yaml | 168 +++++++++---------
1 file changed, 85 insertions(+), 83 deletions(-)
mode change 100644 => 100755 custom_components/adaptive_lighting/services.yaml
diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml
old mode 100644
new mode 100755
index 351fe5b5..c22d1f76
--- a/custom_components/adaptive_lighting/services.yaml
+++ b/custom_components/adaptive_lighting/services.yaml
@@ -2,184 +2,186 @@ apply:
description: Applies the current Adaptive Lighting settings to lights.
fields:
entity_id:
- description: "Entity ID of the switch. \U0001F4DD"
+ description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used.
selector:
entity:
integration: adaptive_lighting
domain: switch
multiple: false
lights:
- description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F"
+ description: entity_id(s) of lights, if not specified, all lights in the switch are selected.
selector:
entity:
domain: light
multiple: true
transition:
- description: "Duration of transition when lights change, in seconds. \U0001F551"
+ description: Transition of the lights.
example: 10
selector:
- text: null
+ text:
adapt_brightness:
- description: "Whether to adapt the brightness of the light. \U0001F31E"
+ description: "Adapt the 'brightness', default: true"
example: true
selector:
- boolean: null
+ boolean:
adapt_color:
- description: "Whether to adapt the color of the light. \U0001F308"
+ description: "Adapt the color_temp/color_rgb, default: true"
example: true
selector:
- boolean: null
+ boolean:
prefer_rgb_color:
- description: "Use RGB color adjustment instead of native light color temperature. \U0001F308"
+ description: "Prefer to use color_rgb over color_temp if possible, default: false"
example: false
selector:
- boolean: null
+ boolean:
turn_on_lights:
- description: "Whether to turn on lights if they are off. \U0001F506"
+ description: "Turn on the lights that are off, default: false"
example: false
selector:
- boolean: null
+ boolean:
+
set_manual_control:
description: Mark whether a light is 'manually controlled'.
fields:
entity_id:
- description: "Entity ID of the switch. \U0001F4DD"
+ description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used.
selector:
entity:
integration: adaptive_lighting
domain: switch
multiple: false
lights:
- description: "List of light entities to be controlled by Adaptive Lighting (may be empty). \U0001F31F"
+ description: entity_id(s) of lights, if not specified, all lights in the switch are selected.
selector:
entity:
domain: light
multiple: true
manual_control:
- description: "Whether to manually control the lights. \U0001F512"
+ description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true"
example: true
default: true
selector:
- boolean: null
+ boolean:
+
change_switch_settings:
- description: Change any settings you'd like in the switch. All options here are the same as in the config flow.
+ description: "Change any settings you'd like in the switch. All options here are the same as in the config flow."
fields:
entity_id:
- description: "Entity ID of the switch. \U0001F4DD"
+ description: "entity_id of the Adaptive Lighting switch."
required: true
selector:
entity:
domain: switch
use_defaults:
- description: "Whether to use default settings for the switches. \u2699\uFE0F"
- example: current
+ description: "(default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation."
+ example: "current"
required: false
- default: current
+ default: "current"
selector:
select:
options:
- - current
- - configuration
- - factory
+ - "current"
+ - "configuration"
+ - "factory"
include_config_in_attributes:
- description: "Show all options as attributes on the switch in Home Assistant when set to `true`. \U0001F4DD"
+ description: "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)"
required: false
selector:
- boolean: null
+ boolean:
turn_on_lights:
- description: "Whether to turn on lights if they are off. \U0001F506"
+ description: "Turn on the lights that are off, default: false"
example: false
required: false
selector:
- boolean: null
+ boolean:
initial_transition:
- description: "Duration of the first transition when lights turn from `off` to `on` in seconds. \u23F2\uFE0F"
+ description: "initial_transition: When lights turn 'off' to 'on'. (seconds)"
example: 1
required: false
selector:
- text: null
+ text:
sleep_transition:
- description: "Duration of transition when 'sleep mode' is toggled in seconds. \U0001F634"
+ description: "sleep_transition: When 'sleep_state' changes. (seconds)"
example: 1
required: false
selector:
- text: null
+ text:
max_brightness:
- description: "Maximum brightness percentage. \U0001F4A1"
+ description: "max_brightness: Highest brightness of lights during a cycle. (%)"
required: false
example: 100
selector:
- text: null
+ text:
max_color_temp:
- description: "Coldest color temperature in Kelvin. \u2744\uFE0F"
+ description: "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)"
required: false
example: 5500
selector:
- text: null
+ text:
min_brightness:
- description: "Minimum brightness percentage. \U0001F4A1"
+ description: "min_brightness: Lowest brightness of lights during a cycle. (%)"
required: false
example: 1
selector:
- text: null
+ text:
min_color_temp:
- description: "Warmest color temperature in Kelvin. \U0001F525"
+ description: "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)"
required: false
example: 2000
selector:
- text: null
+ text:
only_once:
- description: "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). \U0001F504"
+ description: "only_once: Only adapt the lights when turning them on."
example: false
required: false
selector:
- boolean: null
+ boolean:
prefer_rgb_color:
- description: "Use RGB color adjustment instead of native light color temperature. \U0001F308"
+ description: "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible."
required: false
example: false
selector:
- boolean: null
+ boolean:
separate_turn_on_commands:
- description: "Use separate `light.turn_on` calls for color and brightness, needed for some light types. \U0001F500"
+ description: "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights)."
required: false
example: false
selector:
- boolean: null
+ boolean:
send_split_delay:
- description: "Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. \u23F2\uFE0F"
+ description: "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly."
required: false
example: 0
selector:
- boolean: null
+ boolean:
sleep_brightness:
- description: "Brightness percentage of lights in sleep mode. \U0001F634"
+ description: "sleep_brightness, Brightness setting for Sleep Mode. (%)"
required: false
example: 1
selector:
- text: null
+ text:
sleep_rgb_or_color_temp:
- description: "Use either `'rgb_color'` or `'color_temp'` in sleep mode. \U0001F319"
+ description: "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'"
required: false
- example: color_temp
+ example: "color_temp"
selector:
select:
options:
- - rgb_color
- - color_temp
+ - "rgb_color"
+ - "color_temp"
sleep_rgb_color:
- description: "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). \U0001F308"
+ description: "sleep_rgb_color, in RGB"
required: false
selector:
- color_rgb: null
+ color_rgb:
sleep_color_temp:
- description: "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. \U0001F634"
+ description: "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)"
required: false
example: 1000
selector:
- text: null
+ text:
sunrise_offset:
- description: "Adjust sunrise time with a positive or negative offset in seconds. \u23F0"
+ description: sunrise_offset, in +/- seconds (integer)
required: false
example: 0
selector:
@@ -187,64 +189,64 @@ change_switch_settings:
min: 0
max: 86300
sunrise_time:
- description: "Set a fixed time (HH:MM:SS) for sunrise. \U0001F305"
+ description: sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)
required: false
- example: ''
+ example: ""
selector:
- time: null
+ time:
sunset_offset:
- description: "Adjust sunset time with a positive or negative offset in seconds. \u23F0"
+ description: sunset_offset, in +/- seconds (integer)
required: false
- example: ''
+ example: ""
selector:
number:
min: 0
max: 86300
sunset_time:
- description: "Set a fixed time (HH:MM:SS) for sunset. \U0001F307"
- example: ''
+ description: sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)
+ example: ""
required: false
selector:
- time: null
+ time:
max_sunrise_time:
- description: "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. \U0001F305"
- example: ''
+ description: "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)"
+ example: ""
required: false
selector:
- time: null
+ time:
min_sunset_time:
- description: "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. \U0001F307"
- example: ''
+ description: "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)"
+ example: ""
required: false
selector:
- time: null
+ time:
take_over_control:
- description: "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! \U0001F512"
+ description: "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on."
required: false
example: true
selector:
- boolean: null
+ boolean:
detect_non_ha_changes:
- description: "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. \U0001F575\uFE0F"
+ description: "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)"
required: false
example: false
selector:
- boolean: null
+ boolean:
transition:
- description: "Duration of transition when lights change, in seconds. \U0001F551"
+ description: "Transition time when applying a change to the lights (seconds)"
required: false
example: 45
selector:
- text: null
+ text:
adapt_delay:
- description: "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. \u23F2\uFE0F"
+ description: "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering."
required: false
example: 0
selector:
- text: null
+ text:
autoreset_control_seconds:
- description: "Automatically reset the manual control after a number of seconds. Set to 0 to disable. \u23F2\uFE0F"
+ description: "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)"
required: false
example: 0
selector:
- text: null
+ text:
From d768a1e825302b593c7a01f5d57256a04fd30463 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 3 Apr 2023 02:07:05 -0700
Subject: [PATCH 006/549] Release 1.9.1 (#522)
* Bump to 1.9.1
* Add CODEOWNERS
---
.github/CODEOWNERS | 1 +
custom_components/adaptive_lighting/manifest.json | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 .github/CODEOWNERS
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000..847961cb
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @basnijholt
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 803069a7..cb7ba6e1 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": [],
- "version": "1.9.0"
+ "version": "1.9.1"
}
From 26974c8fd5db90f800592912080eca4b816f5710 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 3 Apr 2023 11:32:38 -0700
Subject: [PATCH 007/549] Simplify if-statement, (small #460 fix) (#526)
---
custom_components/adaptive_lighting/switch.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 4b9e1856..031396fe 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1067,7 +1067,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
return
# See #80. Doesn't check if transitions differ but it does the job.
last_service_data = self.turn_on_off_listener.last_service_data
- if light in last_service_data and last_service_data[light] == service_data:
+ if last_service_data.get(light) == service_data:
_LOGGER.debug(
"%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')",
self._name,
From b730c7cc9009be3b7a9a187e15286dcb4d06c6ef Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 3 Apr 2023 14:59:14 -0700
Subject: [PATCH 008/549] Add scripts to auto update en.json, strings.json and
services.yaml (#520)
* Add scripts to auto update strings.json and services.yaml
* Run services
* simplify
* Run strings
* rerun
* revert
* allow unicode
* Add CODEOWNERS
* Update CODEOWNERS
* set CONF_USE_DEFAULTS docs
* add field_name
* Auto run scripts
* Update desc
* Update README.md, strings.json, and services.yaml
* double quotes
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Update README.md, strings.json, and services.yaml
* Add newline
* sync changes between en.json and strings.json
* Update README.md, strings.json, and services.yaml
* double quotes
* fix
* Update README.md, strings.json, and services.yaml
* Add comments
* Remove comments
* shorter
* Update README.md, strings.json, and services.yaml
* Rephrase
* Update README.md, strings.json, and services.yaml
* remove key from desc
---------
Co-authored-by: Benjamin Auquite
Co-authored-by: github-actions[bot]
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.github/update-services.py | 25 +++
.github/update-strings.py | 31 ++++
.github/workflows/update-readme.yml | 14 +-
README.md | 14 +-
custom_components/adaptive_lighting/const.py | 26 +--
.../adaptive_lighting/services.yaml | 169 +++++++++---------
.../adaptive_lighting/strings.json | 60 +++----
.../adaptive_lighting/translations/en.json | 58 +++---
8 files changed, 231 insertions(+), 166 deletions(-)
create mode 100644 .github/update-services.py
create mode 100644 .github/update-strings.py
mode change 100755 => 100644 custom_components/adaptive_lighting/services.yaml
diff --git a/.github/update-services.py b/.github/update-services.py
new file mode 100644
index 00000000..df4c7b30
--- /dev/null
+++ b/.github/update-services.py
@@ -0,0 +1,25 @@
+from pathlib import Path
+import sys
+
+import yaml
+
+sys.path.append(str(Path(__file__).parent.parent))
+
+from custom_components.adaptive_lighting import const # noqa: E402
+
+services_filename = "custom_components/adaptive_lighting/services.yaml"
+with open(services_filename) as f:
+ services = yaml.safe_load(f)
+
+for service_name, dct in services.items():
+ _docs = {"set_manual_control": const.DOCS_MANUAL_CONTROL, "apply": const.DOCS_APPLY}
+ alternative_docs = _docs.get(service_name, const.DOCS)
+ for field_name, field in dct["fields"].items():
+ description = alternative_docs.get(field_name, const.DOCS[field_name])
+ field["description"] = description
+
+comment = "# This file is auto-generated by .github/update-services.py."
+
+with open(services_filename, "w") as f:
+ f.write(comment + "\n")
+ yaml.dump(services, f, sort_keys=False, width=1000, allow_unicode=True)
diff --git a/.github/update-strings.py b/.github/update-strings.py
new file mode 100644
index 00000000..aabc7443
--- /dev/null
+++ b/.github/update-strings.py
@@ -0,0 +1,31 @@
+import json
+from pathlib import Path
+import sys
+
+sys.path.append(str(Path(__file__).parent.parent))
+
+from custom_components.adaptive_lighting import const # noqa: E402
+
+strings_fname = "custom_components/adaptive_lighting/strings.json"
+en_fname = "custom_components/adaptive_lighting/translations/en.json"
+with open(strings_fname) as f:
+ strings = json.load(f)
+
+data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES}
+strings["options"]["step"]["init"]["data"] = data
+
+with open(strings_fname, "w") as f:
+ json.dump(strings, f, indent=2, ensure_ascii=False)
+ f.write("\n")
+
+
+# Sync changes from strings.json to en.json
+with open(en_fname) as f:
+ en = json.load(f)
+
+en["config"]["step"]["user"] = strings["config"]["step"]["user"]
+en["options"]["step"]["init"]["data"] = data
+
+with open(en_fname, "w") as f:
+ json.dump(en, f, indent=2, ensure_ascii=False)
+ f.write("\n")
diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml
index bc31bb94..bec8cb4c 100644
--- a/.github/workflows/update-readme.yml
+++ b/.github/workflows/update-readme.yml
@@ -34,17 +34,23 @@ jobs:
- name: Run markdown-code-runner
run: markdown-code-runner --debug README.md
- - name: Commit updated README.md
+ - name: Run update strings.json
+ run: python .github/update-strings.py
+
+ - name: Run update services.yaml
+ run: python .github/update-services.py
+
+ - name: Commit updated README.md, strings.json, and services.yaml
id: commit
run: |
- git add README.md
+ git add -u .
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
if git diff --quiet && git diff --staged --quiet; then
- echo "No changes in README.md, skipping commit."
+ echo "No changes in README.md, strings.json, and services.yaml, skipping commit."
echo "commit_status=skipped" >> $GITHUB_ENV
else
- git commit -m "Update README.md"
+ git commit -m "Update README.md, strings.json, and services.yaml"
echo "commit_status=committed" >> $GITHUB_ENV
fi
diff --git a/README.md b/README.md
index 5b98de8b..c7a873d0 100644
--- a/README.md
+++ b/README.md
@@ -90,11 +90,11 @@ The YAML and frontend configuration methods support all of the options listed be
| Variable name | Description | Default | Type |
|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
-| `lights` | List of light entities to be controlled by Adaptive Lighting (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` |
| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
-| `sleep_transition` | Duration of transition when 'sleep mode' is toggled in seconds. 😴 | `1` | `float` 0-6553 |
+| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
@@ -103,9 +103,9 @@ The YAML and frontend configuration methods support all of the options listed be
| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
-| `sleep_rgb_or_color_temp` | Use either `'rgb_color'` or `'color_temp'` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
+| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
-| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is 'rgb_color'). 🌈 | `[255, 56, 0]` | RGB color |
+| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
@@ -116,8 +116,8 @@ The YAML and frontend configuration methods support all of the options listed be
| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
-| `send_split_delay` | Wait time (milliseconds) between commands when using `separate_turn_on_commands`. Helps ensure correct handling. ⏲️ | `0` | `int` 0-10000 |
-| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Helps avoid flickering. ⏲️ | `0` | `float > 0` |
+| `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 |
@@ -188,7 +188,7 @@ adaptive_lighting:
|:-------------------------|:-----------------------------------------------------------------------------------------------|:-----------|:---------------------|
| `entity_id` | The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝 | ✅ | list of `entity_id`s |
| `lights` | entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡 | ❌ | list of `entity_id`s |
-| `manual_control` | Whether to add ('true') or remove ('false') the light from the 'manual_control' list. 🔒 | ❌ | bool |
+| `manual_control` | Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒 | ❌ | bool |
#### `adaptive_lighting.change_switch_settings`
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index bb0c07cd..64620927 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -22,9 +22,7 @@ CONF_NAME, DEFAULT_NAME = "name", "default"
DOCS[CONF_NAME] = "Display name for this switch. 📝"
CONF_LIGHTS, DEFAULT_LIGHTS = "lights", []
-DOCS[CONF_LIGHTS] = (
- "List of light entities to be controlled by Adaptive " "Lighting (may be empty). 🌟"
-)
+DOCS[CONF_LIGHTS] = "List of light entity_ids to be controlled (may be empty). 🌟"
CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = (
"detect_non_ha_changes",
@@ -52,7 +50,7 @@ DOCS[CONF_INITIAL_TRANSITION] = (
CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1
DOCS[CONF_SLEEP_TRANSITION] = (
- "Duration of transition when 'sleep mode' is toggled " "in seconds. 😴"
+ 'Duration of transition when "sleep mode" is toggled ' "in seconds. 😴"
)
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90
@@ -102,7 +100,7 @@ DOCS[CONF_SLEEP_COLOR_TEMP] = (
CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0]
DOCS[CONF_SLEEP_RGB_COLOR] = (
- "RGB color in sleep mode (used when " "`sleep_rgb_or_color_temp` is 'rgb_color'). 🌈"
+ "RGB color in sleep mode (used when " '`sleep_rgb_or_color_temp` is "rgb_color"). 🌈'
)
CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = (
@@ -110,7 +108,7 @@ CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = (
"color_temp",
)
DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = (
- "Use either `'rgb_color'` or `'color_temp'` " "in sleep mode. 🌙"
+ 'Use either `"rgb_color"` or `"color_temp"` ' "in sleep mode. 🌙"
)
CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0
@@ -163,13 +161,13 @@ DOCS[CONF_ADAPT_UNTIL_SLEEP] = (
CONF_ADAPT_DELAY, DEFAULT_ADAPT_DELAY = "adapt_delay", 0
DOCS[CONF_ADAPT_DELAY] = (
"Wait time (seconds) between light turn on and Adaptive Lighting applying "
- "changes. Helps avoid flickering. ⏲️"
+ "changes. Might help to avoid flickering. ⏲️"
)
CONF_SEND_SPLIT_DELAY, DEFAULT_SEND_SPLIT_DELAY = "send_split_delay", 0
DOCS[CONF_SEND_SPLIT_DELAY] = (
- "Wait time (milliseconds) between commands when using `separate_turn_on_commands`. "
- "Helps ensure correct handling. ⏲️"
+ "Delay (ms) between `separate_turn_on_commands` for lights that don't support "
+ "simultaneous brightness and color setting. ⏲️"
)
CONF_AUTORESET_CONTROL, DEFAULT_AUTORESET_CONTROL = "autoreset_control_seconds", 0
@@ -197,7 +195,11 @@ CONF_TURN_ON_LIGHTS = "turn_on_lights"
DOCS[CONF_TURN_ON_LIGHTS] = "Whether to turn on lights that are currently off. 🔆"
SERVICE_CHANGE_SWITCH_SETTINGS = "change_switch_settings"
CONF_USE_DEFAULTS = "use_defaults"
-DOCS[CONF_USE_DEFAULTS] = "Whether to use default settings for the switches. ⚙️"
+DOCS[CONF_USE_DEFAULTS] = (
+ "Sets the default values not specified in this service call. Options: "
+ '"current" (default, retains current values), "factory" (resets to '
+ 'documented defaults), or "configuration" (reverts to switch config defaults). ⚙️'
+)
TURNING_OFF_DELAY = 5
@@ -206,8 +208,8 @@ DOCS_MANUAL_CONTROL = {
"light as being `manually controlled`. 📝",
CONF_LIGHTS: "entity_id(s) of lights, if not specified, all lights in the "
"switch are selected. 💡",
- CONF_MANUAL_CONTROL: "Whether to add ('true') or remove ('false') the "
- "light from the 'manual_control' list. 🔒",
+ CONF_MANUAL_CONTROL: 'Whether to add ("true") or remove ("false") the '
+ 'light from the "manual_control" list. 🔒',
}
DOCS_APPLY = {
diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml
old mode 100755
new mode 100644
index c22d1f76..cd25811b
--- a/custom_components/adaptive_lighting/services.yaml
+++ b/custom_components/adaptive_lighting/services.yaml
@@ -1,187 +1,186 @@
+# This file is auto-generated by .github/update-services.py.
apply:
description: Applies the current Adaptive Lighting settings to lights.
fields:
entity_id:
- description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used.
+ description: The `entity_id` of the switch with the settings to apply. 📝
selector:
entity:
integration: adaptive_lighting
domain: switch
multiple: false
lights:
- description: entity_id(s) of lights, if not specified, all lights in the switch are selected.
+ description: A light (or list of lights) to apply the settings to. 💡
selector:
entity:
domain: light
multiple: true
transition:
- description: Transition of the lights.
+ description: Duration of transition when lights change, in seconds. 🕑
example: 10
selector:
- text:
+ text: null
adapt_brightness:
- description: "Adapt the 'brightness', default: true"
+ description: Whether to adapt the brightness of the light. 🌞
example: true
selector:
- boolean:
+ boolean: null
adapt_color:
- description: "Adapt the color_temp/color_rgb, default: true"
+ description: Whether to adapt the color on supporting lights. 🌈
example: true
selector:
- boolean:
+ boolean: null
prefer_rgb_color:
- description: "Prefer to use color_rgb over color_temp if possible, default: false"
+ description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈
example: false
selector:
- boolean:
+ boolean: null
turn_on_lights:
- description: "Turn on the lights that are off, default: false"
+ description: Whether to turn on lights that are currently off. 🔆
example: false
selector:
- boolean:
-
+ boolean: null
set_manual_control:
description: Mark whether a light is 'manually controlled'.
fields:
entity_id:
- description: entity_id of the Adaptive Lighting switch. If not specified, the current Adaptive Lighting switch will be used.
+ description: The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝
selector:
entity:
integration: adaptive_lighting
domain: switch
multiple: false
lights:
- description: entity_id(s) of lights, if not specified, all lights in the switch are selected.
+ description: entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡
selector:
entity:
domain: light
multiple: true
manual_control:
- description: "Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true"
+ description: Whether to add ("true") or remove ("false") the light from the "manual_control" list. 🔒
example: true
default: true
selector:
- boolean:
-
+ boolean: null
change_switch_settings:
- description: "Change any settings you'd like in the switch. All options here are the same as in the config flow."
+ description: Change any settings you'd like in the switch. All options here are the same as in the config flow.
fields:
entity_id:
- description: "entity_id of the Adaptive Lighting switch."
+ description: Entity ID of the switch. 📝
required: true
selector:
entity:
domain: switch
use_defaults:
- description: "(default: 'current' for current settings) You can set this to 'factory', 'configuration', or 'current' to reset the variables not being set with this service call. 'current' leaves them as is, 'configuration' resets to whatever already initializes at startup, 'factory' resets to the default values listed in the documentation."
- example: "current"
+ description: 'Sets the default values not specified in this service call. Options: "current" (default, retains current values), "factory" (resets to documented defaults), or "configuration" (reverts to switch config defaults). ⚙️'
+ example: current
required: false
- default: "current"
+ default: current
selector:
select:
options:
- - "current"
- - "configuration"
- - "factory"
+ - current
+ - configuration
+ - factory
include_config_in_attributes:
- description: "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)"
+ description: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝
required: false
selector:
- boolean:
+ boolean: null
turn_on_lights:
- description: "Turn on the lights that are off, default: false"
+ description: Whether to turn on lights that are currently off. 🔆
example: false
required: false
selector:
- boolean:
+ boolean: null
initial_transition:
- description: "initial_transition: When lights turn 'off' to 'on'. (seconds)"
+ description: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️
example: 1
required: false
selector:
- text:
+ text: null
sleep_transition:
- description: "sleep_transition: When 'sleep_state' changes. (seconds)"
+ description: Duration of transition when "sleep mode" is toggled in seconds. 😴
example: 1
required: false
selector:
- text:
+ text: null
max_brightness:
- description: "max_brightness: Highest brightness of lights during a cycle. (%)"
+ description: Maximum brightness percentage. 💡
required: false
example: 100
selector:
- text:
+ text: null
max_color_temp:
- description: "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)"
+ description: Coldest color temperature in Kelvin. ❄️
required: false
example: 5500
selector:
- text:
+ text: null
min_brightness:
- description: "min_brightness: Lowest brightness of lights during a cycle. (%)"
+ description: Minimum brightness percentage. 💡
required: false
example: 1
selector:
- text:
+ text: null
min_color_temp:
- description: "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)"
+ description: Warmest color temperature in Kelvin. 🔥
required: false
example: 2000
selector:
- text:
+ text: null
only_once:
- description: "only_once: Only adapt the lights when turning them on."
+ description: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄
example: false
required: false
selector:
- boolean:
+ boolean: null
prefer_rgb_color:
- description: "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible."
+ description: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈
required: false
example: false
selector:
- boolean:
+ boolean: null
separate_turn_on_commands:
- description: "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights)."
+ description: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀
required: false
example: false
selector:
- boolean:
+ boolean: null
send_split_delay:
- description: "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly."
+ description: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️
required: false
example: 0
selector:
- boolean:
+ boolean: null
sleep_brightness:
- description: "sleep_brightness, Brightness setting for Sleep Mode. (%)"
+ description: Brightness percentage of lights in sleep mode. 😴
required: false
example: 1
selector:
- text:
+ text: null
sleep_rgb_or_color_temp:
- description: "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'"
+ description: Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙
required: false
- example: "color_temp"
+ example: color_temp
selector:
select:
options:
- - "rgb_color"
- - "color_temp"
+ - rgb_color
+ - color_temp
sleep_rgb_color:
- description: "sleep_rgb_color, in RGB"
+ description: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈
required: false
selector:
- color_rgb:
+ color_rgb: null
sleep_color_temp:
- description: "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)"
+ description: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴
required: false
example: 1000
selector:
- text:
+ text: null
sunrise_offset:
- description: sunrise_offset, in +/- seconds (integer)
+ description: Adjust sunrise time with a positive or negative offset in seconds. ⏰
required: false
example: 0
selector:
@@ -189,64 +188,64 @@ change_switch_settings:
min: 0
max: 86300
sunrise_time:
- description: sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)
+ description: Set a fixed time (HH:MM:SS) for sunrise. 🌅
required: false
- example: ""
+ example: ''
selector:
- time:
+ time: null
sunset_offset:
- description: sunset_offset, in +/- seconds (integer)
+ description: Adjust sunset time with a positive or negative offset in seconds. ⏰
required: false
- example: ""
+ example: ''
selector:
number:
min: 0
max: 86300
sunset_time:
- description: sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)
- example: ""
+ description: Set a fixed time (HH:MM:SS) for sunset. 🌇
+ example: ''
required: false
selector:
- time:
+ time: null
max_sunrise_time:
- description: "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)"
- example: ""
+ description: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅
+ example: ''
required: false
selector:
- time:
+ time: null
min_sunset_time:
- description: "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)"
- example: ""
+ description: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇
+ example: ''
required: false
selector:
- time:
+ time: null
take_over_control:
- description: "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on."
+ description: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒
required: false
example: true
selector:
- boolean:
+ boolean: null
detect_non_ha_changes:
- description: "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)"
+ description: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️
required: false
example: false
selector:
- boolean:
+ boolean: null
transition:
- description: "Transition time when applying a change to the lights (seconds)"
+ description: Duration of transition when lights change, in seconds. 🕑
required: false
example: 45
selector:
- text:
+ text: null
adapt_delay:
- description: "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering."
+ description: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️
required: false
example: 0
selector:
- text:
+ text: null
autoreset_control_seconds:
- description: "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)"
+ description: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️
required: false
example: 0
selector:
- text:
+ text: null
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index 2ebd96d0..54af5e11 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
- "title": "Choose a name for the Adaptive Lighting",
+ "title": "Choose a name for the Adaptive Lighting instance",
"description": "Every instance can contain multiple lights!",
"data": {
"name": "Name"
@@ -19,35 +19,35 @@
"title": "Adaptive Lighting options",
"description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.",
"data": {
- "lights": "lights",
- "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)",
- "include_config_in_attributes": "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)",
- "adapt_until_sleep": "adapt_until_sleep: When checked, Adaptive Lighting will use the sleep settings as the minimum, and transition to these values past the sunset (default: false)",
- "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)",
- "interval": "interval: Time between switch updates. (seconds)",
- "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)",
- "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)",
- "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)",
- "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)",
- "only_once": "only_once: Only adapt the lights when turning them on.",
- "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.",
- "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).",
- "send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.",
- "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'",
- "sleep_rgb_color": "sleep_rgb_color, in RGB",
- "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)",
- "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)",
- "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
- "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
- "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)",
- "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
- "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
- "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.",
- "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)",
- "transition": "Transition time when applying a change to the lights (seconds)",
- "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering.",
- "autoreset_control_seconds": "autoreset_control_seconds: wait time (seconds) before Adaptive Lighting resets `manual_control` status of any light (default: 0)"
+ "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
+ "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
+ "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
+ "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
+ "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
+ "transition": "transition: Duration of transition when lights change, in seconds. 🕑",
+ "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
+ "interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
+ "min_brightness": "min_brightness: Minimum brightness percentage. 💡",
+ "max_brightness": "max_brightness: Maximum brightness percentage. 💡",
+ "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥",
+ "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️",
+ "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
+ "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
+ "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
+ "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
+ "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
+ "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
+ "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
+ "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
+ "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
+ "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
+ "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
+ "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
+ "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
+ "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
+ "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
+ "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️"
}
}
},
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index 51311d64..38fb7b0e 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -4,7 +4,7 @@
"step": {
"user": {
"title": "Choose a name for the Adaptive Lighting instance",
- "description": "Pick a name for this instance. You can run several instances of Adaptive lighting, each of these can contain multiple lights!",
+ "description": "Every instance can contain multiple lights!",
"data": {
"name": "Name"
}
@@ -20,33 +20,35 @@
"title": "Adaptive Lighting options",
"description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.",
"data": {
- "lights": "lights",
- "initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)",
- "include_config_in_attributes": "include_config_in_attributes: All config options will be listed as attributes under the adaptive-lighting switch this integration creates. (default: false)",
- "sleep_transition": "sleep_transition: When 'sleep_state' changes. (seconds)",
- "interval": "interval: Time between switch updates. (seconds)",
- "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)",
- "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)",
- "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)",
- "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)",
- "only_once": "only_once: Only adapt the lights when turning them on.",
- "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.",
- "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).",
- "send_split_delay": "send_split_delay: wait between commands (milliseconds), when separate_turn_on_commands is used. May ensure that both commands are handled by the bulb correctly.",
- "sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, use 'rgb_color' or 'color_temp'",
- "sleep_rgb_color": "sleep_rgb_color, in RGB",
- "sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)",
- "sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- seconds)",
- "sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
- "max_sunrise_time": "max_sunrise_time: Manual override of the maximum sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
- "sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- seconds)",
- "sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
- "min_sunset_time": "min_sunset_time: Manual override of the minimum sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
- "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.",
- "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)",
- "transition": "Transition time when applying a change to the lights (seconds)",
- "adapt_delay": "adapt_delay: wait time between light turn on (seconds), and Adaptive Lights applying changes to the light state. May avoid flickering."
+ "lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
+ "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
+ "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
+ "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
+ "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
+ "transition": "transition: Duration of transition when lights change, in seconds. 🕑",
+ "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
+ "interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
+ "min_brightness": "min_brightness: Minimum brightness percentage. 💡",
+ "max_brightness": "max_brightness: Maximum brightness percentage. 💡",
+ "min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥",
+ "max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️",
+ "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
+ "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
+ "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
+ "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
+ "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
+ "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
+ "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
+ "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
+ "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
+ "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
+ "take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
+ "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
+ "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
+ "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
+ "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
+ "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️"
}
}
},
From c7f44e472ff672512e027f7e8bbe43dbd095558f Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Mon, 3 Apr 2023 19:04:06 -0500
Subject: [PATCH 009/549] Correctly wait for transitions (#510)
* Add auto_reset_manual_control with async timer
* cherry-pick wait for transition stuff
* Update switch.py
* not renamed in this branch yet.
* Update switch.py
* update tests
* Update switch.py
* merge related fix
* cleanup
* Revert "cleanup"
This reverts commit 3aa2f3242b06370c0f84248d2c2c21a2556bb5ec.
* Update switch.py
* Update switch.py
* Update switch.py
* Small refactor
* Move test to old position for better diffs
* Revert "Small refactor"
This reverts commit b986b3f77cba921cbfb07647dea1d6f876883dd4.
* Update README.md
* fix the test
last_state_change isn't updated quick enough.
* #510 changes (#516)
* Change (WIP)
* Update test_switch.py
* Refactor
* Revert "Revert "Small refactor""
This reverts commit 3731c9993676b0a831af12da25bfd8c07ff2a148.
* Update README.md
* Fix the test
* Bump to 1.9.0 (#518)
* Basic community fixes PR (#460)
* Fixes #423
#423
* Do not adapt lights turned on with custom payloads.
* Update switch.py
* Issue fixes
#423, #378, #403, #449
* quickly test #274
* Revert feature requests, this branch only has fixes.
Reverted FR 274
* pre-commit fix
* Create automerge.yaml
* test
* Delete automerge.yaml
My bad.
* Fix #460 and #408
* see @basnijholt 's comment in #450.
* @basnijholt requested changes.
---------
Co-authored-by: Bas Nijholt
* Undo accidental changes introduced in #509, but adds the changes from #460 (#521)
* Release 1.9.1 (#522)
* Bump to 1.9.1
* Add CODEOWNERS
---------
Co-authored-by: Benjamin Auquite
Co-authored-by: github-actions[bot]
* No need to wrap the reset
* Remove unused attrs
* Shorter log message
* revert unrelated tests change
* remove unused function
* Use patch
* Bump to 1.10.0
---------
Co-authored-by: Bas Nijholt
Co-authored-by: github-actions[bot]
Co-authored-by: Bas Nijholt
---
.../adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 261 +++++++++++-------
tests/test_switch.py | 67 ++++-
3 files changed, 211 insertions(+), 119 deletions(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index cb7ba6e1..33564f23 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": [],
- "version": "1.9.1"
+ "version": "1.10.0"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 031396fe..52821774 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import base64
import bisect
-from collections import defaultdict
+from collections.abc import Callable, Coroutine
from copy import deepcopy
from dataclasses import dataclass
import datetime
@@ -802,10 +802,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._only_once = data[CONF_ONLY_ONCE]
self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR]
self._separate_turn_on_commands = data[CONF_SEPARATE_TURN_ON_COMMANDS]
- self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
self._transition = data[CONF_TRANSITION]
self._adapt_delay = data[CONF_ADAPT_DELAY]
self._send_split_delay = data[CONF_SEND_SPLIT_DELAY]
+ self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
+ self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
+ if not data[CONF_TAKE_OVER_CONTROL] and data[CONF_DETECT_NON_HA_CHANGES]:
+ _LOGGER.warning(
+ "%s: Config mismatch: 'detect_non_ha_changes: true' "
+ "requires 'take_over_control' to be enabled. Adjusting config "
+ "and continuing setup with `take_over_control: true`.",
+ self._name,
+ )
+ self._take_over_control = True
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
self._expand_light_groups() # updates manual control timers
_loc = get_astral_location(self.hass)
@@ -1128,11 +1137,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
)
self.async_write_ha_state()
+
if lights is None:
lights = self._lights
- if (self._only_once and not force) or not lights:
+
+ if not force and self._only_once:
return
- await self._adapt_lights(lights, transition, force, context)
+
+ filtered_lights = []
+ for light in lights:
+ # Don't adapt lights that haven't finished prior transitions.
+ if force or not self.turn_on_off_listener.transition_timers.get(light):
+ filtered_lights.append(light)
+
+ if not filtered_lights:
+ return
+
+ await self._adapt_lights(filtered_lights, transition, force, context)
async def _adapt_lights(
self,
@@ -1532,8 +1553,6 @@ class TurnOnOffListener:
self.sleep_tasks: dict[str, asyncio.Task] = {}
# Tracks which lights are manually controlled
self.manual_control: dict[str, bool] = {}
- # Counts the number of times (in a row) a light had a changed state.
- self.cnt_significant_changes: dict[str, int] = defaultdict(int)
# Track 'state_changed' events of self.lights resulting from this integration
self.last_state_change: dict[str, list[State]] = {}
# Track last 'service_data' to 'light.turn_on' resulting from this integration
@@ -1543,9 +1562,8 @@ class TurnOnOffListener:
self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {}
self.auto_reset_manual_control_times: dict[str, float] = {}
- # When a state is different `max_cnt_significant_changes` times in a row,
- # mark it as manually_controlled.
- self.max_cnt_significant_changes = 2
+ # Track light transitions
+ self.transition_timers: dict[str, _AsyncSingleShotTimer] = {}
self.remove_listener = self.hass.bus.async_listen(
EVENT_CALL_SERVICE, self.turn_on_off_event_listener
@@ -1554,6 +1572,56 @@ class TurnOnOffListener:
EVENT_STATE_CHANGED, self.state_changed_event_listener
)
+ def _handle_timer(
+ self,
+ light: str,
+ timers_dict: dict[str, _AsyncSingleShotTimer],
+ delay: float | None,
+ reset_coroutine: Callable[[], Coroutine[Any, Any, None]],
+ ) -> None:
+ timer = timers_dict.get(light)
+ if timer is not None:
+ if delay is None: # Timer object exists, but should not anymore
+ timer.cancel()
+ timers_dict.pop(light)
+ else: # Timer object already exists, just update the delay and restart it
+ timer.delay = delay
+ timer.start()
+ elif delay is not None: # Timer object does not exist, create it
+ timer = _AsyncSingleShotTimer(delay, reset_coroutine)
+ timers_dict[light] = timer
+ timer.start()
+
+ def start_transition_timer(self, light: str) -> None:
+ """Mark a light as manually controlled."""
+ _LOGGER.debug("Start transition timer for %s", light)
+ last_service_data = self.last_service_data
+ if (
+ not last_service_data
+ or light not in last_service_data
+ or ATTR_TRANSITION not in last_service_data[light]
+ ):
+ return
+
+ delay = last_service_data[light][ATTR_TRANSITION]
+
+ async def reset():
+ _LOGGER.debug(
+ "Transition finished for light %s",
+ light,
+ )
+ switches = _get_switches_with_lights(self.hass, [light])
+ for switch in switches:
+ if not switch.is_on:
+ continue
+ await switch._update_attrs_and_maybe_adapt_lights(
+ [light],
+ force=False,
+ context=switch.create_context("transit"),
+ )
+
+ self._handle_timer(light, self.transition_timers, delay, reset)
+
def set_auto_reset_manual_control_times(self, lights: list[str], time: float):
"""Set the time after which the lights are automatically reset."""
if time == 0:
@@ -1576,40 +1644,28 @@ class TurnOnOffListener:
_LOGGER.debug("Marking '%s' as manually controlled.", light)
self.manual_control[light] = True
delay = self.auto_reset_manual_control_times.get(light)
- timer = self.auto_reset_manual_control_timers.get(light)
- if timer is not None:
- if delay is None: # Timer object exists, but should not anymore
- timer.cancel()
- self.auto_reset_manual_control_timers.pop(light)
- else: # Timer object already exists, just update the delay and restart it
- timer.delay = delay
- timer.start()
- elif delay is not None: # Timer object does not exist, create it
- async def reset():
- self.reset(light)
- switches = _get_switches_with_lights(self.hass, [light])
- for switch in switches:
- if not switch.is_on:
- continue
- # pylint: disable=protected-access
- await switch._update_attrs_and_maybe_adapt_lights(
- [light],
- transition=switch._initial_transition,
- force=True,
- context=switch.create_context("autoreset"),
- )
- _LOGGER.debug(
- "Auto resetting 'manual_control' status of '%s' because"
- " it was not manually controlled for %s seconds.",
- light,
- delay,
+ async def reset():
+ self.reset(light)
+ switches = _get_switches_with_lights(self.hass, [light])
+ for switch in switches:
+ if not switch.is_on:
+ continue
+ await switch._update_attrs_and_maybe_adapt_lights(
+ [light],
+ transition=switch._initial_transition,
+ force=True,
+ context=switch.create_context("autoreset"),
)
- assert not self.manual_control[light]
+ _LOGGER.debug(
+ "Auto resetting 'manual_control' status of '%s' because"
+ " it was not manually controlled for %s seconds.",
+ light,
+ delay,
+ )
+ assert not self.manual_control[light]
- timer = _AsyncSingleShotTimer(delay, reset)
- self.auto_reset_manual_control_timers[light] = timer
- timer.start()
+ self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset)
def reset(self, *lights, reset_manual_control=True) -> None:
"""Reset the 'manual_control' status of the lights."""
@@ -1621,7 +1677,6 @@ class TurnOnOffListener:
timer.cancel()
self.last_state_change.pop(light, None)
self.last_service_data.pop(light, None)
- self.cnt_significant_changes[light] = 0
async def turn_on_off_event_listener(self, event: Event) -> None:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
@@ -1700,11 +1755,7 @@ class TurnOnOffListener:
new_state.context.id,
)
- if (
- new_state is not None
- and new_state.state == STATE_ON
- and is_our_context(new_state.context)
- ):
+ if new_state is not None and new_state.state == STATE_ON:
# It is possible to have multiple state change events with the same context.
# This can happen because a `turn_on.light(brightness_pct=100, transition=30)`
# event leads to an instant state change of
@@ -1717,21 +1768,29 @@ class TurnOnOffListener:
# incorrect 'min_kelvin' and 'max_kelvin', which happens e.g., for
# Philips Hue White GU10 Bluetooth lights).
old_state: list[State] | None = self.last_state_change.get(entity_id)
- if (
- old_state is not None
- and old_state[0].context.id == new_state.context.id
- ):
- # If there is already a state change event from this event (with this
- # context) then append it to the already existing list.
- _LOGGER.debug(
- "State change event of '%s' is already in 'self.last_state_change' (%s)"
- " adding this state also",
- entity_id,
- new_state.context.id,
- )
+ if is_our_context(new_state.context):
+ if (
+ old_state is not None
+ and old_state[0].context.id == new_state.context.id
+ ):
+ _LOGGER.debug(
+ "TurnOnOffListener: State change event of '%s' is already"
+ " in 'self.last_state_change' (%s)"
+ " adding this state also",
+ entity_id,
+ new_state.context.id,
+ )
+ self.last_state_change[entity_id].append(new_state)
+ else:
+ _LOGGER.debug(
+ "TurnOnOffListener: New adapt '%s' found for %s",
+ new_state,
+ entity_id,
+ )
+ self.last_state_change[entity_id] = [new_state]
+ self.start_transition_timer(entity_id)
+ elif old_state is not None:
self.last_state_change[entity_id].append(new_state)
- else:
- self.last_state_change[entity_id] = [new_state]
def is_manually_controlled(
self,
@@ -1786,64 +1845,58 @@ class TurnOnOffListener:
detected, we mark the light as 'manually controlled' until the light
or switch is turned 'off' and 'on' again.
"""
- if light not in self.last_state_change:
- return False
- old_states: list[State] = self.last_state_change[light]
- await self.hass.helpers.entity_component.async_update_entity(light)
- new_state = self.hass.states.get(light)
+ last_service_data = self.last_service_data.get(light)
+ if last_service_data is None:
+ return
compare_to = functools.partial(
_attributes_have_changed,
light=light,
- new_attributes=new_state.attributes,
adapt_brightness=adapt_brightness,
adapt_color=adapt_color,
context=context,
)
- for index, old_state in enumerate(old_states):
- changed = compare_to(old_attributes=old_state.attributes)
- if not changed:
- _LOGGER.debug(
- "State of '%s' didn't change wrt change event nr. %s (context.id=%s)",
- light,
- index,
- context.id,
- )
- break
-
- last_service_data = self.last_service_data.get(light)
- if changed and last_service_data is not None:
- # It can happen that the state change events that are associated
- # with the last 'light.turn_on' call by this integration were not
- # final states. Possibly a later EVENT_STATE_CHANGED happened, where
- # the correct target brightness/color was reached.
- changed = compare_to(old_attributes=last_service_data)
- if not changed:
+ # Update state and check for a manual change not done in HA.
+ # Ensure HASS is correctly updating your light's state with
+ # light.turn_on calls if any problems arise. This
+ # can happen e.g. using zigbee2mqtt with 'report: false' in device settings.
+ if switch._detect_non_ha_changes:
+ _LOGGER.debug(
+ "%s: 'detect_non_ha_changes: true', calling update_entity(%s)"
+ " and check if it's last adapt succeeded.",
+ switch._name,
+ light,
+ )
+ # This update_entity probably isn't necessary now that we're checking
+ # if transitions finished from our last adapt.
+ await self.hass.helpers.entity_component.async_update_entity(light)
+ refreshed_state = self.hass.states.get(light)
+ _LOGGER.debug(
+ "%s: Current state of %s: %s",
+ switch._name,
+ light,
+ refreshed_state,
+ )
+ changed = compare_to(
+ old_attributes=last_service_data,
+ new_attributes=refreshed_state.attributes,
+ )
+ if changed:
_LOGGER.debug(
"State of '%s' didn't change wrt 'last_service_data' (context.id=%s)",
light,
context.id,
)
-
- n_changes = self.cnt_significant_changes[light]
- if changed:
- self.cnt_significant_changes[light] += 1
- if n_changes >= self.max_cnt_significant_changes:
- # Only mark a light as significantly changing, if changed==True
- # N times in a row. We do this because sometimes a state changes
- # happens only *after* a new update interval has already started.
self.mark_as_manual_control(light)
_fire_manual_control_event(switch, light, context, is_async=False)
- else:
- if n_changes > 1:
- _LOGGER.debug(
- "State of '%s' had 'cnt_significant_changes=%s' but the state"
- " changed to the expected settings now",
- light,
- n_changes,
- )
- self.cnt_significant_changes[light] = 0
-
- return changed
+ return True
+ _LOGGER.debug(
+ "%s: Light '%s' correctly matches our last adapt's service data, continuing..."
+ " context.id=%s.",
+ switch._name,
+ light,
+ context.id,
+ )
+ return False
async def maybe_cancel_adjusting(
self, entity_id: str, off_to_on_event: Event, on_to_off_event: Event | None
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 212cea82..9e88b332 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -56,6 +56,7 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_AREA_ID,
ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
CONF_LIGHTS,
CONF_NAME,
CONF_PLATFORM,
@@ -769,25 +770,63 @@ async def test_significant_change(hass):
)
await hass.async_block_till_done()
- switch, (bed_light_instance, *_) = await setup_lights_and_switch(hass)
+ async def set_brightness(val: int):
+ hass.states.async_set(
+ ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1}
+ )
+ await hass.async_block_till_done()
+
+ switch, _ = await setup_lights_and_switch(hass)
+ _LOGGER.debug("Test detect_non_ha_changes:")
+ switch._take_over_control = True
+ assert switch._take_over_control
+ switch._detect_non_ha_changes = True
+ assert switch._detect_non_ha_changes
+
+ # build last service data
+ await update(force=False)
+
+ # force=True should not reset manual control.
+ await turn_light(True, brightness=40)
+ await turn_light(True, brightness=20)
+ await update(force=False)
+ assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+ await update(force=True)
+ assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+
+ # turn light off then on should reset manual control.
+ await turn_light(False)
await turn_light(True)
- await update(force=True) # removes manual control
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
- # Change brightness by setting state (not using 'light.turn_on')
- attributes = hass.states.get(ENTITY_LIGHT).attributes
- new_attributes = attributes.copy()
- new_brightness = (attributes[ATTR_BRIGHTNESS] + 100) % 255
- new_attributes[ATTR_BRIGHTNESS] = new_brightness
- bed_light_instance._brightness = new_brightness
+ # Assert last_service_data got filled from update()
+ await update(force=True)
assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
- for _ in range(switch.turn_on_off_listener.max_cnt_significant_changes):
+
+ # Simulate a transition to 255 where the update() is already using brightness 255.
+ await set_brightness(240)
+ await set_brightness(244)
+ await set_brightness(247)
+ await set_brightness(250)
+
+ # last_state_change should have our state changes.
+ # Change brightness by async_set (not using 'light.turn_on')
+ new_brightness = 50
+ await set_brightness(new_brightness)
+ _LOGGER.debug("Test: Brightness set to %s", new_brightness)
+
+ # mock homeassistant.core.HomeAssistant.helpers.entity_component.async_update_entity
+ # Otherwise what happens is update_entity() refreshes the state to the last call of
+ # light.turn_on(). This is because we are not using hass.states.async_set() to
+ # set the brightness of the light. We mock `async_update_ha_state` because
+ # `async_update_entity` calls it.
+ with patch("homeassistant.helpers.entity.Entity.async_update_ha_state"):
+ # On next update ENTITY_LIGHT should be marked as manually controlled
await update(force=False)
- assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
- # On next update the light should be marked as manually controlled
- await update(force=False)
- # TODO: the state should be `bool(manual_control) is True`
- assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+ assert (
+ switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
+ )
+ assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
def test_color_difference_redmean():
From 981287edb96d04e012c5ab689bc15bb981135b89 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 3 Apr 2023 17:30:07 -0700
Subject: [PATCH 010/549] [pre-commit.ci] pre-commit autoupdate (#531)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2f8938a7..462e0dc2 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -12,7 +12,7 @@ repos:
hooks:
- id: flake8
- repo: https://github.com/psf/black
- rev: 23.1.0
+ rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/asottile/pyupgrade
From be0735002c5814ec822ca5435d530569dafa62e4 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Mon, 3 Apr 2023 21:59:14 -0700
Subject: [PATCH 011/549] docs: add th3w1zard1 as a contributor for bug (#534)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 3 ++-
README.md | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 344b3112..3872e09b 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -400,7 +400,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/2219836?v=4",
"profile": "https://github.com/th3w1zard1",
"contributions": [
- "code"
+ "code",
+ "bug"
]
},
{
diff --git a/README.md b/README.md
index c7a873d0..65fc0d74 100644
--- a/README.md
+++ b/README.md
@@ -445,7 +445,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting
 Tomáš Valigura 🌍 |
-  Benjamin Auquite 💻 |
+  Benjamin Auquite 💻 🐛 |
 Skyler Carlson 📖 |
 Chris 💻 |
 Raman Gupta 💻 |
From e4d06476fd0eb3fa6f90c368edc30860a85d844b Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Tue, 4 Apr 2023 01:47:08 -0500
Subject: [PATCH 012/549] Add windows command for Docker test instructions
(#536)
* ( Tiny Change ) Add windows command for dockertest
You said it earlier but the correct command for running the Docker image on windows is:
```bash
docker run -v %cd%:/app basnijholt/adaptive-lighting:latest
```
* Update README.md
---------
Co-authored-by: Bas Nijholt
---
tests/README.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/tests/README.md b/tests/README.md
index adedcc4c..a576d518 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -5,10 +5,18 @@ Alternatively, you can use the provided Docker image to run the tests locally.
To run the tests using the Docker image, navigate to the `adaptive-lighting` repo folder and execute the following command:
+Linux or MacOS:
+
```bash
docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest
```
+Windows:
+
+```bash
+docker run -v %cd%:/app basnijholt/adaptive-lighting:latest
+```
+
This command will download the Docker image from [the adaptive-lighting Docker Hub repo](https://hub.docker.com/r/basnijholt/adaptive-lighting) and run the tests.
If you prefer to build the image yourself, use the following command:
From 744e43f4bf057775125de2ffc576d9902df8e530 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Tue, 4 Apr 2023 08:54:54 -0700
Subject: [PATCH 013/549] docs: add th3w1zard1 as a contributor for maintenance
(#538)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 3 ++-
README.md | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 3872e09b..6c825dad 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -401,7 +401,8 @@
"profile": "https://github.com/th3w1zard1",
"contributions": [
"code",
- "bug"
+ "bug",
+ "maintenance"
]
},
{
diff --git a/README.md b/README.md
index 65fc0d74..f013d6c5 100644
--- a/README.md
+++ b/README.md
@@ -445,7 +445,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting
 Tomáš Valigura 🌍 |
-  Benjamin Auquite 💻 🐛 |
+  Benjamin Auquite 💻 🐛 🚧 |
 Skyler Carlson 📖 |
 Chris 💻 |
 Raman Gupta 💻 |
From 4fcf238360f9cd4528d89f3c17c86f44ac61ec3a Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Tue, 4 Apr 2023 22:52:54 -0500
Subject: [PATCH 014/549] Update README.md on transition_until_sleep parameter
(#539)
* Update README.md
I believe you changed the config option's name after I posted the graph, so I renamed the config option there too.
There was also a deleted user on the contributions list so I went ahead and removed that too.
* chore(docs): update TOC
* Update README.md
---------
Co-authored-by: th3w1zard1
---
README.md | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index f013d6c5..9ba7cc03 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,9 @@

-Adaptive Lighting is a custom component for Home Assistant that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control. Try it out now by finding it in HACS (Home Assistant Community Store) and installing it!
+Adaptive Lighting is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control.
+
+Download and install directly through [HACS (Home Assistant Community Store)](https://hacs.xyz/)
By automatically adapting the settings of your lights throughout the day, Adaptive Lighting helps maintain your natural circadian rhythm 😴, which can lead to improved sleep, mood, and overall well-being. Experience cooler color temperatures at noon, gradually transitioning to warmer colors at sunset and sunrise.
@@ -58,7 +60,7 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as
- [:sunny: Sun Position](#sunny-sun-position)
- [:thermometer: Color Temperature](#thermometer-color-temperature)
- [:high_brightness: Brightness](#high_brightness-brightness)
- - [While using `adapt_until_sleep: true`](#while-using-adapt_until_sleep-true)
+ - [While using `transition_until_sleep: true`](#while-using-transition_until_sleep-true)
- [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors)
@@ -379,7 +381,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting
#### :high_brightness: Brightness

-#### While using `adapt_until_sleep: true`
+#### While using `transition_until_sleep: true`

@@ -421,7 +423,6 @@ These graphs were generated using the values calculated by the Adaptive Lighting
 Hudson Brendon 🌍 |
 Gabriel Visser 📖 |
 Gleb 🌍 |
-  Deleted user 🌍 |
 Avi Miller 📖 💻 |
 Denys Dovhan 🌍 |
 David Stenbeck 📖 |
From f5abf034c4653777d8c8b0e4e51b556caeb2660c Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Wed, 5 Apr 2023 11:13:57 -0500
Subject: [PATCH 015/549] Fix the docker tests instructions (#543)
* Tested on multiple hardware
Turns out windows 10 and 11 can't use `$(pwd):/app` OR `%cd%:/app` in PowerShell (which replaced cmd prompt), so I looked up the docs and made the necessary changes (again, sorry!)
These changes have been tested on all terminal environments except macOS (the docs say it'll work there)
* allow use of --exitfirst for faster debug
* Remove install in actions
---------
Co-authored-by: Bas Nijholt
Co-authored-by: Bas Nijholt
---
tests/README.md | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/tests/README.md b/tests/README.md
index a576d518..1c472541 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -5,17 +5,15 @@ Alternatively, you can use the provided Docker image to run the tests locally.
To run the tests using the Docker image, navigate to the `adaptive-lighting` repo folder and execute the following command:
-Linux or MacOS:
-
+Linux / MacOS / Windows PowerShell:
```bash
-docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest
+docker run -v ${PWD}:/app basnijholt/adaptive-lighting:latest
```
-Windows:
-
-```bash
-docker run -v %cd%:/app basnijholt/adaptive-lighting:latest
-```
+- In windows command prompt, the command is:
+ ```bash
+ docker run -v %cd%:/app basnijholt/adaptive-lighting:latest
+ ```
This command will download the Docker image from [the adaptive-lighting Docker Hub repo](https://hub.docker.com/r/basnijholt/adaptive-lighting) and run the tests.
From 03a2d9cbf67964ec3cae59f4f9d6ace32d113d99 Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Wed, 5 Apr 2023 18:39:31 -0500
Subject: [PATCH 016/549] Fix RGB Color Temp Swaps (#514)
* cherry pick from 486
* Refactor `_add_missing_attributes`
---------
Co-authored-by: Bas Nijholt
---
custom_components/adaptive_lighting/switch.py | 56 ++++++++++++++-----
tests/test_switch.py | 27 +++++++--
2 files changed, 64 insertions(+), 19 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 52821774..347ee4cb 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -85,6 +85,7 @@ from homeassistant.util.color import (
color_RGB_to_xy,
color_temperature_to_rgb,
color_xy_to_hs,
+ color_xy_to_RGB,
)
import homeassistant.util.dt as dt_util
import voluptuous as vol
@@ -628,6 +629,41 @@ def color_difference_redmean(
return math.sqrt(red_term + green_term + blue_term)
+# All comparisons should be done with RGB since
+# converting anything to color temp is inaccurate.
+def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]:
+ if ATTR_RGB_COLOR in attributes:
+ return attributes
+
+ rgb = None
+ if ATTR_COLOR_TEMP_KELVIN in attributes:
+ rgb = color_temperature_to_rgb(attributes[ATTR_COLOR_TEMP_KELVIN])
+ elif ATTR_XY_COLOR in attributes:
+ rgb = color_xy_to_RGB(*attributes[ATTR_XY_COLOR])
+
+ if rgb is not None:
+ attributes[ATTR_RGB_COLOR] = rgb
+ _LOGGER.debug(f"Converted {attributes} to rgb {rgb}")
+ else:
+ _LOGGER.debug("No suitable conversion found")
+
+ return attributes
+
+
+def _add_missing_attributes(
+ old_attributes: dict[str, Any],
+ new_attributes: dict[str, Any],
+) -> dict[str, Any]:
+ if not any(
+ attr in old_attributes and attr in new_attributes
+ for attr in [ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR]
+ ):
+ old_attributes = _convert_attributes(old_attributes)
+ new_attributes = _convert_attributes(new_attributes)
+
+ return old_attributes, new_attributes
+
+
def _attributes_have_changed(
light: str,
old_attributes: dict[str, Any],
@@ -636,6 +672,11 @@ def _attributes_have_changed(
adapt_color: bool,
context: Context,
) -> bool:
+ if adapt_color:
+ old_attributes, new_attributes = _add_missing_attributes(
+ old_attributes, new_attributes
+ )
+
if (
adapt_brightness
and ATTR_BRIGHTNESS in old_attributes
@@ -690,21 +731,6 @@ def _attributes_have_changed(
context.id,
)
return True
-
- switched_color_temp = (
- ATTR_RGB_COLOR in old_attributes and ATTR_RGB_COLOR not in new_attributes
- )
- switched_to_rgb_color = (
- ATTR_COLOR_TEMP_KELVIN in old_attributes
- and ATTR_COLOR_TEMP_KELVIN not in new_attributes
- )
- if switched_color_temp or switched_to_rgb_color:
- # Light switched from RGB mode to color_temp or visa versa
- _LOGGER.debug(
- "'%s' switched from RGB mode to color_temp or visa versa",
- light,
- )
- return True
return False
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 9e88b332..db4900dc 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -47,6 +47,7 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
+ ATTR_XY_COLOR,
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.light import SERVICE_TURN_OFF
@@ -873,10 +874,28 @@ def test_attributes_have_changed():
assert _attributes_have_changed(
old_attributes=attributes_1, new_attributes=attrs, **kwargs
)
- # Switch from rgb_color to color_temp
- assert _attributes_have_changed(
- old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 100},
- new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0)},
+ _LOGGER.debug("Test switch from color_temp to rgb_color")
+ assert not _attributes_have_changed(
+ old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702},
+ new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)},
+ **kwargs,
+ )
+ _LOGGER.debug("Test switch from rgb_color to color_temp")
+ assert not _attributes_have_changed(
+ old_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)},
+ new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702},
+ **kwargs,
+ )
+ _LOGGER.debug("Test switch from color_temp to color_xy")
+ assert not _attributes_have_changed(
+ old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702},
+ new_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)},
+ **kwargs,
+ )
+ _LOGGER.debug("Test switch from color_xy to color_temp")
+ assert not _attributes_have_changed(
+ old_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)},
+ new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702},
**kwargs,
)
From cb967aeeb7ab7e5ee3818587da8975a5ad3439cd Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Thu, 6 Apr 2023 13:53:23 -0500
Subject: [PATCH 017/549] Create intentionally over-redundant `state_change`
tests and fix #541 (#544)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* add transition_timer test and debug
* syntax error
* test
* Update switch.py
* Revert "test"
This reverts commit b8009e0a1a4c419ddb026a4545b38453158d9367.
* Update test_switch.py
* add `create_transition_events` to tests.
nearly done
* tests are done!
* pop is for dictionaries
* Update test_switch.py
* combine the tests
* pin markdown-code-runner
* Pin with '=='
* Update test_switch.py
* pin in the correct place 😅
* Update test_switch.py
* Use timer.is_running
* Update test_switch.py
* ensure timer is running in tests
* this passes the test
* Update test_switch.py
* Do not create new list when not needed
* Remove empty deps
* Remove CONF_ULID_MAX_LENGTH (which is not configurable)
* this shouldn't pass the test but it does.
---------
Co-authored-by: Bas Nijholt
Co-authored-by: Bas Nijholt
---
.github/workflows/update-readme.yml | 2 +-
custom_components/adaptive_lighting/switch.py | 64 +--
tests/test_switch.py | 396 ++++++++++++++----
3 files changed, 346 insertions(+), 116 deletions(-)
diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml
index bec8cb4c..1f661333 100644
--- a/.github/workflows/update-readme.yml
+++ b/.github/workflows/update-readme.yml
@@ -24,7 +24,7 @@ jobs:
- name: Install markdown-code-runner and README code dependencies
run: |
- pip install markdown-code-runner pandas tabulate
+ pip install markdown-code-runner==1.0.0 pandas tabulate
- name: Link custom_components/adaptive_lighting
run: |
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 347ee4cb..dbf3c42b 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1102,7 +1102,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
return
# See #80. Doesn't check if transitions differ but it does the job.
last_service_data = self.turn_on_off_listener.last_service_data
- if last_service_data.get(light) == service_data:
+ if not force and last_service_data.get(light) == service_data:
_LOGGER.debug(
"%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')",
self._name,
@@ -1167,14 +1167,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if lights is None:
lights = self._lights
- if not force and self._only_once:
- return
-
filtered_lights = []
- for light in lights:
- # Don't adapt lights that haven't finished prior transitions.
- if force or not self.turn_on_off_listener.transition_timers.get(light):
- filtered_lights.append(light)
+ if not force:
+ if self._only_once:
+ return
+ for light in lights:
+ # Don't adapt lights that haven't finished prior transitions.
+ timer = self.turn_on_off_listener.transition_timers.get(light)
+ if timer is not None and timer.is_running():
+ _LOGGER.debug(
+ "%s: Light '%s' is still transitioning",
+ self._name,
+ light,
+ )
+ else:
+ filtered_lights.append(light)
+ else:
+ filtered_lights = lights
if not filtered_lights:
return
@@ -1620,33 +1629,28 @@ class TurnOnOffListener:
def start_transition_timer(self, light: str) -> None:
"""Mark a light as manually controlled."""
- _LOGGER.debug("Start transition timer for %s", light)
- last_service_data = self.last_service_data
- if (
- not last_service_data
- or light not in last_service_data
- or ATTR_TRANSITION not in last_service_data[light]
- ):
+ last_service_data = self.last_service_data.get(light)
+ if not last_service_data:
+ _LOGGER.debug("This should not ever happen. Please report to the devs.")
return
-
- delay = last_service_data[light][ATTR_TRANSITION]
+ last_transition = last_service_data.get(ATTR_TRANSITION)
+ if not last_transition:
+ _LOGGER.debug(
+ "No transition in last adapt for light %s, continuing...", light
+ )
+ return
+ _LOGGER.debug(
+ "Start transition timer of %s seconds for light %s", last_transition, light
+ )
async def reset():
+ ValueError("TEST")
_LOGGER.debug(
"Transition finished for light %s",
light,
)
- switches = _get_switches_with_lights(self.hass, [light])
- for switch in switches:
- if not switch.is_on:
- continue
- await switch._update_attrs_and_maybe_adapt_lights(
- [light],
- force=False,
- context=switch.create_context("transit"),
- )
- self._handle_timer(light, self.transition_timers, delay, reset)
+ self._handle_timer(light, self.transition_timers, last_transition, reset)
def set_auto_reset_manual_control_times(self, lights: list[str], time: float):
"""Set the time after which the lights are automatically reset."""
@@ -1769,7 +1773,7 @@ class TurnOnOffListener:
async def state_changed_event_listener(self, event: Event) -> None:
"""Track 'state_changed' events."""
entity_id = event.data.get(ATTR_ENTITY_ID, "")
- if entity_id not in self.lights or entity_id.split(".")[0] != LIGHT_DOMAIN:
+ if entity_id not in self.lights:
return
new_state = event.data.get("new_state")
@@ -1814,6 +1818,10 @@ class TurnOnOffListener:
entity_id,
)
self.last_state_change[entity_id] = [new_state]
+ _LOGGER.debug(
+ "Last transition: %s",
+ self.last_service_data[entity_id].get(ATTR_TRANSITION),
+ )
self.start_transition_timer(entity_id)
elif old_state is not None:
self.last_state_change[entity_id].append(new_state)
diff --git a/tests/test_switch.py b/tests/test_switch.py
index db4900dc..6ba54a99 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -1,9 +1,12 @@
"""Tests for Adaptive Lighting switches."""
# pylint: disable=protected-access
import asyncio
+from copy import deepcopy
import datetime
import logging
+from random import choices as random_choices
from random import randint
+import string
from unittest.mock import patch
from homeassistant.components.adaptive_lighting.const import (
@@ -47,6 +50,7 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
+ ATTR_TRANSITION,
ATTR_XY_COLOR,
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
@@ -61,6 +65,7 @@ from homeassistant.const import (
CONF_LIGHTS,
CONF_NAME,
CONF_PLATFORM,
+ EVENT_STATE_CHANGED,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
@@ -107,6 +112,11 @@ ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}"
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
+GLOBAL_TEST_DEPENDENCIES = [
+ "test_adaptive_lighting_switches",
+ "test_light_settings",
+]
+
@pytest.fixture
def reset_time_zone():
@@ -209,6 +219,69 @@ async def setup_lights_and_switch(hass, extra_conf=None):
return switch, lights_instances
+def create_random_context() -> str:
+ ulid_max_length = 26 # changed from 36->26 in core2023.4.0
+ return Context(
+ id="".join(
+ random_choices(string.ascii_uppercase + string.digits, k=ulid_max_length)
+ ),
+ parent_id=None,
+ )
+
+
+# see https://github.com/home-assistant/core/blob/dev/homeassistant/scripts/benchmark/__init__.py
+# basically just search the repo for EVENT_STATE_CHANGED look for how it's fired.
+def create_transition_events(
+ light: str,
+ state: State,
+ last: dict | None = None,
+ current: dict | None = None,
+ total_events: int = 4,
+) -> list[dict]:
+ assert light is not None
+ all_events = []
+ for i in range(1, total_events):
+ # Build basic event data.
+ attributes = {}
+
+ # The first state change always has the context from our integration.
+ # That one will not be in all_events.
+ # It's very possible it stores the parent_id though.
+ # If it stores the parent_id in all situations, there's a great improvement
+ # that could added in future updates.
+
+ # Simulate the events the bulb would send to HASS.
+ last_brightness = last.get(ATTR_BRIGHTNESS) or state[ATTR_BRIGHTNESS]
+ current_brightness = current.get(ATTR_BRIGHTNESS)
+ if (
+ last_brightness
+ and current_brightness
+ and last_brightness != current_brightness
+ ):
+ diff = (current_brightness - last_brightness) * (i / total_events)
+ attributes[ATTR_BRIGHTNESS] = last_brightness + diff
+ elif current_brightness:
+ attributes[ATTR_BRIGHTNESS] = current_brightness
+ current_kelvin = current.get(ATTR_COLOR_TEMP_KELVIN)
+ last_kelvin = last.get(ATTR_COLOR_TEMP_KELVIN) or state[ATTR_COLOR_TEMP_KELVIN]
+ if last_kelvin and current_kelvin and last_kelvin != current_kelvin:
+ diff = (current_kelvin - last_kelvin) * (i / total_events)
+ attributes[ATTR_COLOR_TEMP_KELVIN] = last_kelvin + diff
+ elif current_kelvin:
+ attributes[ATTR_COLOR_TEMP_KELVIN] = current_kelvin
+
+ # Pack event
+ event_data = {
+ ATTR_ENTITY_ID: light,
+ "old_state": State(light, "on", attributes=last),
+ "new_state": State(
+ light, "on", attributes=attributes, context=create_random_context()
+ ),
+ }
+ all_events.append(event_data)
+ return all_events
+
+
async def test_adaptive_lighting_switches(hass):
"""Test switches created for adaptive_lighting integration."""
entry, _ = await setup_switch(hass, {})
@@ -236,6 +309,7 @@ async def test_adaptive_lighting_switches(hass):
@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS)
+@pytest.mark.dependency("test_adaptive_lighting_switches")
async def test_adaptive_lighting_time_zones_with_default_settings(
hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name
):
@@ -428,6 +502,7 @@ async def test_light_settings(hass):
assert_expected_color_temp(state)
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_turn_on_off_listener_not_tracking_untracked_lights(hass):
"""Test that lights that are not in a Adaptive Lighting switch aren't tracked."""
switch, _ = await setup_lights_and_switch(hass)
@@ -447,6 +522,7 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass):
assert light not in switch.turn_on_off_listener.lights
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_manual_control(hass):
"""Test the 'manual control' tracking."""
switch, (light, *_) = await setup_lights_and_switch(hass)
@@ -594,6 +670,7 @@ async def test_manual_control(hass):
assert all([not manual_control[eid] for eid in switch._lights])
+@pytest.mark.dependency(depends=[*GLOBAL_TEST_DEPENDENCIES, "test_manual_control"])
async def test_auto_reset_manual_control(hass):
switch, (light, *_) = await setup_lights_and_switch(
hass, {CONF_AUTORESET_CONTROL: 0.1}
@@ -638,6 +715,7 @@ async def test_auto_reset_manual_control(hass):
assert not manual_control[light.entity_id]
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_apply_service(hass):
"""Test adaptive_lighting.apply service."""
switch, (_, _, light) = await setup_lights_and_switch(hass)
@@ -701,6 +779,9 @@ async def test_apply_service(hass):
assert old_state[ATTR_COLOR_TEMP_KELVIN] == new_state[ATTR_COLOR_TEMP_KELVIN]
+@pytest.mark.dependency(
+ depends=[*GLOBAL_TEST_DEPENDENCIES, "test_apply_service", "test_manual_control"]
+)
async def test_switch_off_on_off(hass):
"""Test switch rapid off_on_off."""
@@ -751,85 +832,7 @@ async def test_switch_off_on_off(hass):
assert state == STATE_OFF
-async def test_significant_change(hass):
- """Test significant change."""
-
- async def turn_light(state, **kwargs):
- await hass.services.async_call(
- LIGHT_DOMAIN,
- SERVICE_TURN_ON if state else SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs},
- blocking=True,
- )
- await hass.async_block_till_done()
-
- async def update(force):
- await switch._update_attrs_and_maybe_adapt_lights(
- transition=0,
- context=switch.create_context("test"),
- force=force,
- )
- await hass.async_block_till_done()
-
- async def set_brightness(val: int):
- hass.states.async_set(
- ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1}
- )
- await hass.async_block_till_done()
-
- switch, _ = await setup_lights_and_switch(hass)
- _LOGGER.debug("Test detect_non_ha_changes:")
- switch._take_over_control = True
- assert switch._take_over_control
- switch._detect_non_ha_changes = True
- assert switch._detect_non_ha_changes
-
- # build last service data
- await update(force=False)
-
- # force=True should not reset manual control.
- await turn_light(True, brightness=40)
- await turn_light(True, brightness=20)
- await update(force=False)
- assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
- await update(force=True)
- assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
-
- # turn light off then on should reset manual control.
- await turn_light(False)
- await turn_light(True)
- assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
-
- # Assert last_service_data got filled from update()
- await update(force=True)
- assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
-
- # Simulate a transition to 255 where the update() is already using brightness 255.
- await set_brightness(240)
- await set_brightness(244)
- await set_brightness(247)
- await set_brightness(250)
-
- # last_state_change should have our state changes.
- # Change brightness by async_set (not using 'light.turn_on')
- new_brightness = 50
- await set_brightness(new_brightness)
- _LOGGER.debug("Test: Brightness set to %s", new_brightness)
-
- # mock homeassistant.core.HomeAssistant.helpers.entity_component.async_update_entity
- # Otherwise what happens is update_entity() refreshes the state to the last call of
- # light.turn_on(). This is because we are not using hass.states.async_set() to
- # set the brightness of the light. We mock `async_update_ha_state` because
- # `async_update_entity` calls it.
- with patch("homeassistant.helpers.entity.Entity.async_update_ha_state"):
- # On next update ENTITY_LIGHT should be marked as manually controlled
- await update(force=False)
- assert (
- switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
- )
- assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
-
-
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
def test_color_difference_redmean():
"""Test color_difference_redmean function."""
for _ in range(10):
@@ -839,14 +842,6 @@ def test_color_difference_redmean():
color_difference_redmean((0, 0, 0), (255, 255, 255))
-def test_is_our_context():
- """Test is_our_context function."""
- context = create_context(DOMAIN, "test", 0)
- assert is_our_context(context)
- assert not is_our_context(None)
- assert not is_our_context(Context())
-
-
def test_attributes_have_changed():
"""Test _attributes_have_changed function."""
attributes_1 = {
@@ -900,6 +895,229 @@ def test_attributes_have_changed():
)
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
+async def test_state_change_handlers(hass):
+ """
+ Test TurnOnOffListener's EVENT_STATE_CHANGED listener.
+ ======================
+ Sequence of events:
+ 1. Transition from sleep mode to normal.
+ 2. Create simulated transition events for that adapt.
+ 3. Fire all simulated transition events.
+ 4. Assert all possible problems that would result.
+ Also tests significant changes.
+ """
+ switch, (light, *_) = await setup_lights_and_switch(hass)
+ context = switch.create_context("test") # needs to be passed to update method
+
+ # [Config options]:
+ transition_used = 2
+ total_events = 5
+
+ async def set_brightness(val: int):
+ # 'Unsafe' set but we know what we're doing.
+ hass.states.async_set(
+ ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1}
+ )
+ await hass.async_block_till_done()
+ # Call code in TurnOnOffListener
+ hass.bus.async_fire(
+ EVENT_STATE_CHANGED,
+ {
+ "new_state": {
+ ATTR_ENTITY_ID: ENTITY_LIGHT,
+ "state": "on",
+ ATTR_BRIGHTNESS: val,
+ }
+ },
+ )
+ await hass.async_block_till_done()
+
+ async def turn_light(state, **kwargs):
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON if state else SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ async def update(force: bool = False):
+ await switch._update_attrs_and_maybe_adapt_lights(
+ force=force, transition=0, context=context
+ )
+ await hass.async_block_till_done()
+
+ # 1. Adapt to sleep without a transition.
+ # Should only be one state change.
+ _LOGGER.debug('test_state_change_handling: Turn on "sleep mode"')
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT)
+ assert len(switch.turn_on_off_listener.last_state_change[ENTITY_LIGHT]) == 1
+ assert not switch.turn_on_off_listener.transition_timers.get(ENTITY_LIGHT)
+ last_service_data = deepcopy(switch.turn_on_off_listener.last_service_data)
+ assert last_service_data.get(ENTITY_LIGHT)
+
+ # 2 Adapt from sleep with a 'transition'.
+ await switch.sleep_mode_switch.async_turn_off()
+ await switch._update_attrs_and_maybe_adapt_lights(
+ force=False, transition=0, context=context
+ )
+ await hass.async_block_till_done()
+ current_service_data = switch.turn_on_off_listener.last_service_data
+ assert current_service_data != last_service_data
+
+ for light in switch._lights:
+ # current_service_data should have changed after the last update.
+ assert current_service_data.get(light)
+ assert last_service_data.get(light)
+ assert current_service_data[light] != last_service_data[light]
+
+ # Test same context id events.
+ current_service_data[light][ATTR_TRANSITION] = transition_used
+ hass.bus.async_fire(
+ EVENT_STATE_CHANGED,
+ {
+ ATTR_ENTITY_ID: light,
+ "old_state": State(light, "on", attributes=last_service_data),
+ "new_state": State(
+ light, "on", attributes=current_service_data, context=context
+ ),
+ },
+ )
+ assert not switch.turn_on_off_listener.transition_timers.get(light)
+
+ # 2.3 Refire and overwrite the original state_changed event with our 'transition'
+ hass.bus.async_fire(
+ EVENT_STATE_CHANGED,
+ {
+ ATTR_ENTITY_ID: light,
+ "old_state": State(light, "on", attributes=last_service_data),
+ "new_state": State(
+ light,
+ "on",
+ attributes=current_service_data,
+ # We need to overwrite the old context_id
+ context=switch.create_context("test"),
+ ),
+ },
+ )
+ await hass.async_block_till_done()
+ # Assert our transition timer was created.
+ assert switch.turn_on_off_listener.transition_timers.get(light)
+ # 2.5 Simulate a transition. There's no other way to do this in the demo.
+ events = create_transition_events(
+ light=light,
+ state=hass.states.get(light),
+ last=last_service_data[light],
+ current=current_service_data[light],
+ total_events=total_events,
+ )
+ # 3. Fire simulated events for our TurnOnOffListener
+ for event in events:
+ _LOGGER.debug("Test EVENT_STATE_CHANGED listener")
+ hass.bus.async_fire(EVENT_STATE_CHANGED, event)
+ await hass.async_block_till_done()
+ # On real systems HA fires transition state changes every ~3 seconds.
+ # asyncio.sleep(3)
+ # 4. Assert the transition timer started and everything was filled.
+ listener = switch.turn_on_off_listener
+ assert listener.last_state_change.get(ENTITY_LIGHT)
+ assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events
+ assert listener.transition_timers.get(ENTITY_LIGHT)
+
+ # 5. Execute some checks during a transition
+ _LOGGER.debug("Test detect_non_ha_changes:")
+ switch._take_over_control = True
+ assert switch._take_over_control
+ switch._detect_non_ha_changes = True
+ assert switch._detect_non_ha_changes
+ 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.
+ 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
+ await update(force=False)
+
+ # force=True should not reset manual control.
+ await turn_light(True, brightness=40)
+ await turn_light(True, brightness=20)
+ await update(force=False)
+ assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+ await update(force=True)
+ assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+
+ # turn light off then on should reset manual control.
+ await turn_light(False)
+ await turn_light(True)
+ assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+
+ # last_state_change should have our state changes.
+ # Change brightness by async_set (not using 'light.turn_on')
+ new_brightness = 50
+ await set_brightness(new_brightness)
+ _LOGGER.debug("Test: Brightness set to %s", new_brightness)
+
+ # mock homeassistant.core.HomeAssistant.helpers.entity_component.async_update_entity
+ # Otherwise what happens is update_entity() refreshes the state to the last call of
+ # light.turn_on(). This is because we are not using hass.states.async_set() to
+ # set the brightness of the light. We mock `async_update_ha_state` because
+ # `async_update_entity` calls it.
+ with patch("homeassistant.helpers.entity.Entity.async_update_ha_state"):
+ # On next update ENTITY_LIGHT should be marked as manually controlled
+ await update(force=False)
+ assert (
+ 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(
+ depends=[
+ *GLOBAL_TEST_DEPENDENCIES,
+ "test_manual_control",
+ "test_apply_service",
+ "test_attributes_have_changed",
+ "test_state_change_handling",
+ ]
+)
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
+def test_is_our_context():
+ """Test is_our_context function."""
+ context = create_context(DOMAIN, "test", 0)
+ assert is_our_context(context)
+ assert not is_our_context(None)
+ assert not is_our_context(Context())
+
+
async def test_unload_switch(hass):
"""Test removing Adaptive Lighting."""
entry, _ = await setup_switch(hass, {})
@@ -966,6 +1184,7 @@ async def test_turn_on_and_off_when_already_at_that_state(hass):
await hass.async_block_till_done()
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_async_update_at_interval(hass):
"""Test '_async_update_at_interval' method."""
_, switch = await setup_switch(hass, {})
@@ -973,6 +1192,7 @@ async def test_async_update_at_interval(hass):
@pytest.mark.parametrize("separate_turn_on_commands", (True, False))
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_separate_turn_on_commands(hass, separate_turn_on_commands):
"""Test 'separate_turn_on_commands' argument."""
switch, (light, *_) = await setup_lights_and_switch(
@@ -1009,6 +1229,7 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands):
assert sleep_color_temp != color_temp
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_area(hass):
switch, (light, *_) = await setup_lights_and_switch(hass)
@@ -1045,6 +1266,7 @@ async def test_area(hass):
assert light.entity_id not in switch.turn_on_off_listener.last_service_data
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_change_switch_settings_service(hass):
"""Test adaptive_lighting.change_switch_settings service."""
switch, (_, _, light) = await setup_lights_and_switch(hass)
From 59877a034340793ec1c8d70d42916ed5116d174d Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 6 Apr 2023 12:37:02 -0700
Subject: [PATCH 018/549] Make sure context_id is 26 chars and partially
conform to ULID standard (#550)
---
.../adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 72 +++++++++++++++----
tests/test_switch.py | 17 ++---
3 files changed, 63 insertions(+), 28 deletions(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 33564f23..166b3695 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -7,6 +7,6 @@
"documentation": "https://github.com/basnijholt/adaptive-lighting#readme",
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
- "requirements": [],
+ "requirements": ["ulid-transform"],
"version": "1.10.0"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index dbf3c42b..5b816b67 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -88,6 +88,7 @@ from homeassistant.util.color import (
color_xy_to_RGB,
)
import homeassistant.util.dt as dt_util
+import ulid_transform
import voluptuous as vol
from .const import (
@@ -182,21 +183,58 @@ BRIGHTNESS_ATTRS = {
}
# Keep a short domain version for the context instances (which can only be 36 chars)
-_DOMAIN_SHORT = "adapt_lgt"
+_DOMAIN_SHORT = "al"
-def _int_to_bytes(i: int, signed: bool = False) -> bytes:
- bits = i.bit_length()
- if signed:
- # Make room for the sign bit.
- bits += 1
- return i.to_bytes((bits + 7) // 8, "little", signed=signed)
+def _int_to_base36(num: int) -> str:
+ """
+ Convert an integer to its base-36 representation using numbers and uppercase letters.
+
+ Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive
+ alphanumeric representation. The function takes an integer `num` as input and returns
+ its base-36 representation as a string.
+
+ Parameters
+ ----------
+ num
+ The integer to convert to base-36.
+
+ Returns
+ -------
+ str
+ The base-36 representation of the input integer.
+
+ Examples
+ --------
+ >>> num = 123456
+ >>> base36_num = int_to_base36(num)
+ >>> print(base36_num)
+ '2N9'
+ """
+ ALPHANUMERIC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+ if num == 0:
+ return ALPHANUMERIC_CHARS[0]
+
+ base36_str = ""
+ base = len(ALPHANUMERIC_CHARS)
+
+ while num:
+ num, remainder = divmod(num, base)
+ base36_str = ALPHANUMERIC_CHARS[remainder] + base36_str
+
+ return base36_str
def _short_hash(string: str, length: int = 4) -> str:
"""Create a hash of 'string' with length 'length'."""
- str_hash_bytes = _int_to_bytes(hash(string), signed=True)
- return base64.b85encode(str_hash_bytes)[:length]
+ return base64.b32encode(string.encode()).decode("utf-8").zfill(length)[:length]
+
+
+def _remove_vowels(input_str: str, length: int = 4) -> str:
+ vowels = "aeiouAEIOU"
+ output_str = "".join([char for char in input_str if char not in vowels])
+ return output_str.zfill(length)[:length]
def create_context(
@@ -204,12 +242,16 @@ def create_context(
) -> Context:
"""Create a context that can identify this integration."""
# Use a hash for the name because otherwise the context might become
- # too long (max len == 36) to fit in the database.
- name_hash = _short_hash(name)
+ # too long (max len == 26) to fit in the database.
# Pack index with base85 to maximize the number of contexts we can create
- # before we exceed the 36-character limit and are forced to wrap.
- index_packed = base64.b85encode(_int_to_bytes(index, signed=False))
- context_id = f"{_DOMAIN_SHORT}:{name_hash}:{which}:{index_packed}"[:36]
+ # before we exceed the 26-character limit and are forced to wrap.
+ time_stamp = ulid_transform.ulid_now()[:10] # time part of a ULID
+ name_hash = _short_hash(name)
+ which_short = _remove_vowels(which)
+ context_id_start = f"{time_stamp}:{_DOMAIN_SHORT}:{name_hash}:{which_short}:"
+ chars_left = 26 - len(context_id_start)
+ index_packed = _int_to_base36(index).zfill(chars_left)[-chars_left:]
+ context_id = context_id_start + index_packed
parent_id = parent.id if parent else None
return Context(id=context_id, parent_id=parent_id)
@@ -218,7 +260,7 @@ def is_our_context(context: Context | None) -> bool:
"""Check whether this integration created 'context'."""
if context is None:
return False
- return context.id.startswith(_DOMAIN_SHORT)
+ return f":{_DOMAIN_SHORT}:" in context.id
def _split_service_data(service_data, adapt_brightness, adapt_color):
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 6ba54a99..ae722b0d 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -4,9 +4,7 @@ import asyncio
from copy import deepcopy
import datetime
import logging
-from random import choices as random_choices
from random import randint
-import string
from unittest.mock import patch
from homeassistant.components.adaptive_lighting.const import (
@@ -76,6 +74,7 @@ from homeassistant.setup import async_setup_component
from homeassistant.util.color import color_temperature_mired_to_kelvin
import homeassistant.util.dt as dt_util
import pytest
+import ulid_transform
import voluptuous.error
from tests.common import MockConfigEntry, mock_area_registry
@@ -118,6 +117,10 @@ GLOBAL_TEST_DEPENDENCIES = [
]
+def create_random_context() -> str:
+ return Context(id=ulid_transform.ulid_now(), parent_id=None)
+
+
@pytest.fixture
def reset_time_zone():
"""Reset time zone."""
@@ -219,16 +222,6 @@ async def setup_lights_and_switch(hass, extra_conf=None):
return switch, lights_instances
-def create_random_context() -> str:
- ulid_max_length = 26 # changed from 36->26 in core2023.4.0
- return Context(
- id="".join(
- random_choices(string.ascii_uppercase + string.digits, k=ulid_max_length)
- ),
- parent_id=None,
- )
-
-
# see https://github.com/home-assistant/core/blob/dev/homeassistant/scripts/benchmark/__init__.py
# basically just search the repo for EVENT_STATE_CHANGED look for how it's fired.
def create_transition_events(
From a01fec02113c46404d155205c9778913d640c3f0 Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Thu, 6 Apr 2023 15:29:32 -0500
Subject: [PATCH 019/549] Bump to 1.10.1 (#551)
* Update manifest.json
* undo merge mistake
* Version 1.10.1
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 166b3695..92b27dfa 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.10.0"
+ "version": "1.10.1"
}
From c0c363136bcc90190081f1f6a6eff52ec6e25c21 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 6 Apr 2023 16:36:57 -0700
Subject: [PATCH 020/549] Test multiple Home Assistant releases and the dev
branch (#552)
* Test for multiple Home Assistant versions
* Install ulid-transform
* remove unnecessary unsafe `async_set` from test
* Skip test_state_change_handlers in <2023.4
* Revert "Skip test_state_change_handlers in <2023.4"
This reverts commit 8d01b6ec4ea8b97feb8dabf8b737be604755dffd.
---------
Co-authored-by: Benjamin Auquite
---
.../workflows/install_dependencies/action.yml | 12 ++++++---
.github/workflows/pytest.yaml | 5 +++-
.github/workflows/update-readme.yml | 2 +-
tests/test_switch.py | 27 +++++--------------
4 files changed, 21 insertions(+), 25 deletions(-)
diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml
index 75820072..df0bee8a 100644
--- a/.github/workflows/install_dependencies/action.yml
+++ b/.github/workflows/install_dependencies/action.yml
@@ -1,10 +1,14 @@
name: 'Install Dependencies'
description: 'Install Home Assistant and test dependencies'
inputs:
- python_version:
+ python-version:
description: 'Python version'
required: true
default: '3.10'
+ core-version:
+ description: 'Home Assistant core version'
+ required: false
+ default: 'dev'
runs:
using: "composite"
@@ -21,11 +25,12 @@ runs:
with:
repository: home-assistant/core
path: core
- - name: Set up Python ${{ inputs.python_version }}
+ ref: ${{ inputs.core-version }}
+ - name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@v4.1.0
with:
- python-version: ${{ inputs.python_version }}
+ python-version: ${{ inputs.python-version }}
- name: Install dependencies
shell: bash
run: |
@@ -33,4 +38,5 @@ runs:
pip install -r core/requirements.txt --use-pep517
pip install -r core/requirements_test.txt --use-pep517
pip install -e core/ --use-pep517
+ pip install ulid-transform # this is in Adaptive-lighting's manifest.json
pip install $(python test_dependencies.py) --use-pep517
diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml
index 02bf93e3..cea75602 100644
--- a/.github/workflows/pytest.yaml
+++ b/.github/workflows/pytest.yaml
@@ -11,8 +11,10 @@ jobs:
runs-on: ubuntu-20.04
timeout-minutes: 60
strategy:
+ fail-fast: false
matrix:
python-version: ["3.10"]
+ core-version: ["2023.2.5", "2023.3.6", "2023.4.0", "dev"]
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3
@@ -20,7 +22,8 @@ jobs:
- name: Install Home Assistant
uses: ./.github/workflows/install_dependencies
with:
- python_version: ${{ matrix.python-version }}
+ python-version: ${{ matrix.python-version }}
+ core-version: ${{ matrix.core-version }}
- name: Click here for troubleshooting steps if tests break again.
run: |
diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml
index 1f661333..2c2754fb 100644
--- a/.github/workflows/update-readme.yml
+++ b/.github/workflows/update-readme.yml
@@ -20,7 +20,7 @@ jobs:
- name: Install Home Assistant
uses: ./.github/workflows/install_dependencies
with:
- python_version: "3.10"
+ python-version: "3.10"
- name: Install markdown-code-runner and README code dependencies
run: |
diff --git a/tests/test_switch.py b/tests/test_switch.py
index ae722b0d..f6d671f4 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -1070,27 +1070,14 @@ async def test_state_change_handlers(hass):
await turn_light(True)
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
- # last_state_change should have our state changes.
- # Change brightness by async_set (not using 'light.turn_on')
- new_brightness = 50
- await set_brightness(new_brightness)
- _LOGGER.debug("Test: Brightness set to %s", new_brightness)
+ await turn_light(True, brightness=50)
+ _LOGGER.debug("Test: Brightness set to %s", 50)
- # mock homeassistant.core.HomeAssistant.helpers.entity_component.async_update_entity
- # Otherwise what happens is update_entity() refreshes the state to the last call of
- # light.turn_on(). This is because we are not using hass.states.async_set() to
- # set the brightness of the light. We mock `async_update_ha_state` because
- # `async_update_entity` calls it.
- with patch("homeassistant.helpers.entity.Entity.async_update_ha_state"):
- # On next update ENTITY_LIGHT should be marked as manually controlled
- await update(force=False)
- assert (
- 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]
+ # 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(
From 39e9d0e74fde95c9d35505a63316c8728ae895a4 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 7 Apr 2023 17:40:36 -0700
Subject: [PATCH 021/549] Update issue templates (#557)
---
.github/ISSUE_TEMPLATE/bug-report.md | 68 ++++++++++++++++++++++-----
.github/ISSUE_TEMPLATE/doc.md | 7 ++-
.github/ISSUE_TEMPLATE/enhancement.md | 7 ++-
.github/ISSUE_TEMPLATE/feature.md | 7 ++-
4 files changed, 72 insertions(+), 17 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
index c5bcb3a4..385259a6 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -1,17 +1,63 @@
---
-name: 'Bug Report'
-about: 'Report a bug in adaptive-lighting.'
-labels: kind/bug, need/triage
+name: Bug Report
+about: Report a bug in adaptive-lighting.
+title: ''
+labels: kind/bug, kind/feature, need/triage
+assignees: ''
+
---
-#### Version information:
+# Home Assistant Adaptive Lighting Issue Template
+
+## Bug Reports
+
+If you need help with using or configuring Adaptive Lighting, please [open a Q&A discussion thread here](https://github.com/basnijholt/adaptive-lighting/discussions/new?category=q-a) instead.
+
+### Before submitting a bug report, please follow these troubleshooting steps:
+
+Please confirm that you have completed the following steps:
+
+- [ ] I have updated to the [latest Adaptive Lighting version](https://github.com/basnijholt/adaptive-lighting/releases) available in [HACS](https://hacs.xyz/).
+- [ ] I have reviewed the [Troubleshooting Section](https://github.com/basnijholt/adaptive-lighting#troubleshooting) in the [README](https://github.com/basnijholt/adaptive-lighting#readme).
+- [ ] (If using Zigbee2MQTT) I have read the [Zigbee2MQTT troubleshooting guide](https://github.com/basnijholt/adaptive-lighting#zigbee2mqtt) in the [README](https://github.com/basnijholt/adaptive-lighting#readme).
+- [ ] I have checked the [V2 Roadmap](https://github.com/basnijholt/adaptive-lighting/discussions/291) and [open issues](https://github.com/basnijholt/adaptive-lighting/issues) to ensure my issue isn't a duplicate.
-#### Description:
-
+Please include the following information in your issue.
+
+*Issues missing this information may not be addressed.*
+
+1. **Debug logs** captured while the issue occurred. [See here for instructions on enabling debug logging](https://github.com/basnijholt/adaptive-lighting#troubleshooting):
+
+```
+
+```
+
+2. [Your Adaptive Lighting configuration](https://github.com/basnijholt/adaptive-lighting#gear-configuration):
+
+```
+
+```
+
+3. (If using Zigbee2MQTT), provide your configuration files (**remove all personal information before posting**):
+ - `devices.yaml`
+ - `groups.yaml`
+ - `configuration.yaml` ⚠️; **Warning** _**REMOVE ALL of the PERSONAL INFORMATION BELOW before posting**_ ⚠️;
+ - mqtt: `server`:
+ - mqtt: `user`:
+ - mqtt: `password`:
+ - advanced: `pan_id`:
+ - advanced: `network_key`:
+ - anything in `log_syslog` if you use this
+ - Brand and model number of problematic light(s)
+```
+
+```
+
+4. Describe the bug and how to reproduce it:
+
+
+
+5. Steps to reproduce the behavior:
diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md
index 98c9a008..f6458d77 100644
--- a/.github/ISSUE_TEMPLATE/doc.md
+++ b/.github/ISSUE_TEMPLATE/doc.md
@@ -1,7 +1,10 @@
---
-name: 'Documentation Issue'
-about: 'Report missing, erroneous docs, broken links or propose new docs'
+name: Documentation Issue
+about: Report missing, erroneous docs, broken links or propose new docs
+title: ''
labels: kind/docs_issue, need/triage
+assignees: ''
+
---
#### Location
diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md
index cc515a20..d25a9689 100644
--- a/.github/ISSUE_TEMPLATE/enhancement.md
+++ b/.github/ISSUE_TEMPLATE/enhancement.md
@@ -1,5 +1,8 @@
---
-name: 'Enhancement'
-about: 'Suggest an improvement to an existing feature.'
+name: Enhancement
+about: Suggest an improvement to an existing feature.
+title: ''
labels: kind/enhancement, need/triage
+assignees: ''
+
---
diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md
index c4b787df..088b35e4 100644
--- a/.github/ISSUE_TEMPLATE/feature.md
+++ b/.github/ISSUE_TEMPLATE/feature.md
@@ -1,5 +1,8 @@
---
-name: 'Feature'
-about: 'Suggest a new feature'
+name: Feature
+about: Suggest a new feature
+title: ''
labels: kind/feature, need/triage
+assignees: ''
+
---
From fe7bdd394014df77785bdf598a37e41bd3d3ae44 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 8 Apr 2023 03:39:08 -0700
Subject: [PATCH 022/549] Add auto_reset_time_remaining attribute (#558)
* Add auto_reset_time_remaining attribute
* fix attr
* Add test
---
custom_components/adaptive_lighting/switch.py | 13 +++++++++++++
tests/test_switch.py | 6 ++++++
2 files changed, 19 insertions(+)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 5b816b67..884f74cd 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1014,6 +1014,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if self.turn_on_off_listener.manual_control.get(light)
]
extra_state_attributes.update(self._settings)
+ timers = self.turn_on_off_listener.auto_reset_manual_control_timers
+ extra_state_attributes["autoreset_time_remaining"] = {
+ light: time
+ for light in self._lights
+ if (timer := timers.get(light)) and (time := timer.remaining_time()) > 0
+ }
return extra_state_attributes
def create_context(
@@ -2106,3 +2112,10 @@ class _AsyncSingleShotTimer:
if self.task:
self.task.cancel()
self.callback = None
+
+ def remaining_time(self):
+ """Return the remaining time before the timer expires."""
+ if self.start_time is not None:
+ elapsed_time = (dt_util.utcnow() - self.start_time).total_seconds()
+ return max(0, self.delay - elapsed_time)
+ return 0
diff --git a/tests/test_switch.py b/tests/test_switch.py
index f6d671f4..476e83cc 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -692,9 +692,15 @@ async def test_auto_reset_manual_control(hass):
await turn_light(True, brightness=1)
await turn_light(True, brightness=10)
assert manual_control[light.entity_id]
+ assert (
+ switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0
+ )
await asyncio.sleep(0.3) # Should be enough time for auto reset
await update()
assert not manual_control[light.entity_id], (light, manual_control)
+ assert (
+ light.entity_id not in switch.extra_state_attributes["autoreset_time_remaining"]
+ )
# Do a couple of quick changes and check that light is not reset
for i in range(3):
From e30b7debe551e58cc6a738f7d15a4bb75d9bb545 Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Sat, 8 Apr 2023 20:54:06 -0500
Subject: [PATCH 023/549] Refactor `_adapt_lights` into
`_update_manual_control_and_maybe_adapt` (#513)
* Add auto_reset_manual_control with async timer
* cherry-pick wait for transition stuff
* Update switch.py
* merge wait_for_transition
* Update switch.py
* not renamed in this branch yet.
* Update switch.py
* update tests
* Update switch.py
* Update switch.py
* merge related fix
* cleanup
* Revert "cleanup"
This reverts commit 3aa2f3242b06370c0f84248d2c2c21a2556bb5ec.
* 0.1 sometimes fails the test
* not in this pr yet
* Update switch.py
* Update switch.py
* Update switch.py
* Update switch.py
* Update README.md, strings.json, and services.yaml
* slight refactor
* Update switch.py
* Update switch.py
* Possible refactor of _update_attrs_and_maybe_adapt_lights
* cleaned up
* Revert "cleaned up"
This reverts commit 441cb1ff5cc45eb3438f7e367e8f22cea061ba73.
* Possible refactor of _update_attrs_and_maybe_adapt_lights (#537)
Co-authored-by: Benjamin Auquite
* Revert "Revert "cleaned up""
This reverts commit 11b268148b098af4f790a6f41c8b87b14ea0748a.
* remove bad merge conflict
* revert permissions
* Bump to 1.11.0
* Undo unrelated test changes
* 'else' instead of 'continue'
---------
Co-authored-by: Bas Nijholt
Co-authored-by: github-actions[bot]
Co-authored-by: Bas Nijholt
---
.../adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 91 ++++++++++---------
2 files changed, 50 insertions(+), 43 deletions(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 92b27dfa..2dec4c83 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.10.1"
+ "version": "1.11.0"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 884f74cd..98747a17 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -424,6 +424,7 @@ def _fire_manual_control_event(
switch.entity_id,
light,
)
+ switch.turn_on_off_listener.mark_as_manual_control(light)
fire(
f"{DOMAIN}.manual_control",
{ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id},
@@ -519,7 +520,6 @@ async def async_setup_entry(
all_lights = _expand_light_groups(switch.hass, lights)
if service_call.data[CONF_MANUAL_CONTROL]:
for light in all_lights:
- switch.turn_on_off_listener.mark_as_manual_control(light)
_fire_manual_control_event(switch, light, service_call.context)
else:
switch.turn_on_off_listener.reset(*all_lights)
@@ -1088,9 +1088,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if lock is not None and lock.locked():
_LOGGER.debug("%s: '%s' is locked", self._name, light)
return
- service_data = {ATTR_ENTITY_ID: light}
- features = _supported_features(self.hass, light)
-
if transition is None:
transition = self._transition
if adapt_brightness is None:
@@ -1100,15 +1097,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if prefer_rgb_color is None:
prefer_rgb_color = self._prefer_rgb_color
- # Check transition == 0 to fix #378
- if "transition" in features and transition > 0:
- service_data[ATTR_TRANSITION] = transition
-
# The switch might be off and not have _settings set.
self._settings = self._sun_light_settings.get_settings(
self.sleep_mode_switch.is_on, transition
)
+ # Build service data.
+ service_data = {ATTR_ENTITY_ID: light}
+ features = _supported_features(self.hass, light)
+
+ # Check transition == 0 to fix #378
+ if "transition" in features and transition > 0:
+ service_data[ATTR_TRANSITION] = transition
if "brightness" in features and adapt_brightness:
brightness = round(255 * self._settings["brightness_pct"] / 100)
service_data[ATTR_BRIGHTNESS] = brightness
@@ -1135,19 +1135,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
context = context or self.create_context("adapt_lights")
- if (
- self._take_over_control
- and self._detect_non_ha_changes
- and not force
- and await self.turn_on_off_listener.significant_change(
- self,
- light,
- adapt_brightness,
- adapt_color,
- context,
- )
- ):
- return
+
# See #80. Doesn't check if transitions differ but it does the job.
last_service_data = self.turn_on_off_listener.last_service_data
if not force and last_service_data.get(light) == service_data:
@@ -1236,9 +1224,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if not filtered_lights:
return
- await self._adapt_lights(filtered_lights, transition, force, context)
+ await self._update_manual_control_and_maybe_adapt(
+ filtered_lights, transition, force, context
+ )
- async def _adapt_lights(
+ async def _update_manual_control_and_maybe_adapt(
self,
lights: list[str],
transition: int | None,
@@ -1247,34 +1237,53 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
) -> None:
assert context is not None
_LOGGER.debug(
- "%s: '_adapt_lights(%s, %s, force=%s, context.id=%s)' called",
+ "%s: '_update_manual_control_and_maybe_adapt(%s, %s, force=%s, context.id=%s)' called",
self.name,
lights,
transition,
force,
context.id,
)
+
+ adapt_brightness = self.adapt_brightness_switch.is_on
+ adapt_color = self.adapt_color_switch.is_on
+
for light in lights:
if not is_on(self.hass, light):
continue
- if (
- self._take_over_control
- and self.turn_on_off_listener.is_manually_controlled(
+
+ manually_controlled = self.turn_on_off_listener.is_manually_controlled(
+ self,
+ light,
+ force,
+ adapt_brightness,
+ adapt_color,
+ )
+
+ significant_change = (
+ self._detect_non_ha_changes
+ and not force
+ and await self.turn_on_off_listener.significant_change(
self,
light,
- force,
- self.adapt_brightness_switch.is_on,
- self.adapt_color_switch.is_on,
+ adapt_brightness,
+ adapt_color,
+ context,
)
- ):
- _LOGGER.debug(
- "%s: '%s' is being manually controlled, stop adapting, context.id=%s.",
- self._name,
- light,
- context.id,
- )
- continue
- await self._adapt_light(light, transition, force=force, context=context)
+ )
+
+ if self._take_over_control and (manually_controlled or significant_change):
+ if manually_controlled:
+ _LOGGER.debug(
+ "%s: '%s' is being manually controlled, stop adapting, context.id=%s.",
+ self._name,
+ light,
+ context.id,
+ )
+ else:
+ _fire_manual_control_event(self, light, context)
+ else:
+ await self._adapt_light(light, transition, force=force, context=context)
async def _sleep_mode_switch_state_event(self, event: Event) -> None:
if not match_switch_state_event(event, (STATE_ON, STATE_OFF)):
@@ -1900,7 +1909,7 @@ class TurnOnOffListener:
):
# Light was already on and 'light.turn_on' was not called by
# the adaptive_lighting integration.
- manual_control = self.mark_as_manual_control(light)
+ manual_control = True
_fire_manual_control_event(switch, light, turn_on_event.context)
_LOGGER.debug(
"'%s' was already on and 'light.turn_on' was not called by the"
@@ -1968,8 +1977,6 @@ class TurnOnOffListener:
light,
context.id,
)
- self.mark_as_manual_control(light)
- _fire_manual_control_event(switch, light, context, is_async=False)
return True
_LOGGER.debug(
"%s: Light '%s' correctly matches our last adapt's service data, continuing..."
From a888afd6dcdfd335a22e45f8b6be0bbd959ebd87 Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Mon, 10 Apr 2023 12:07:31 -0500
Subject: [PATCH 024/549] Refactor `_supported_features` (#565)
* initial commit
* change features to dict
* another slight refactor
---
custom_components/adaptive_lighting/const.py | 2 +
custom_components/adaptive_lighting/switch.py | 95 +++++++++++++------
2 files changed, 66 insertions(+), 31 deletions(-)
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index 64620927..6d36ae2e 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -273,6 +273,8 @@ VALIDATION_TUPLES = [
),
]
+CONST_COLOR = "color"
+
def timedelta_as_int(value):
"""Convert a `datetime.timedelta` object to an integer.
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 98747a17..3eabf7ff 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -23,7 +23,11 @@ from homeassistant.components.light import (
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
+ ATTR_MAX_COLOR_TEMP_KELVIN,
+ ATTR_MIN_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
+ ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
ATTR_XY_COLOR,
@@ -32,6 +36,7 @@ from homeassistant.components.light import (
COLOR_MODE_HS,
COLOR_MODE_RGB,
COLOR_MODE_RGBW,
+ COLOR_MODE_RGBWW,
COLOR_MODE_XY,
)
from homeassistant.components.light import (
@@ -129,6 +134,7 @@ from .const import (
CONF_TRANSITION,
CONF_TURN_ON_LIGHTS,
CONF_USE_DEFAULTS,
+ CONST_COLOR,
DOMAIN,
EXTRA_VALIDATION,
ICON_BRIGHTNESS,
@@ -155,6 +161,16 @@ _SUPPORT_OPTS = {
"transition": SUPPORT_TRANSITION,
}
+VALID_COLOR_MODES = {
+ COLOR_MODE_BRIGHTNESS: ATTR_BRIGHTNESS,
+ COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP_KELVIN,
+ COLOR_MODE_HS: ATTR_HS_COLOR,
+ COLOR_MODE_RGB: ATTR_RGB_COLOR,
+ COLOR_MODE_RGBW: ATTR_RGBW_COLOR,
+ COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR,
+ COLOR_MODE_XY: ATTR_XY_COLOR,
+}
+
_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT)
_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))}
@@ -623,33 +639,51 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
return list(all_lights)
+def _supported_to_attributes(supported):
+ supported_attributes = {}
+ supports_colors = False
+ for mode, attr in VALID_COLOR_MODES.items():
+ if mode not in supported:
+ continue
+ supported_attributes[attr] = True
+ if (
+ not supports_colors
+ and mode != COLOR_MODE_BRIGHTNESS
+ and mode != COLOR_MODE_COLOR_TEMP
+ ):
+ supports_colors = True
+ return supported_attributes, supports_colors
+
+
def _supported_features(hass: HomeAssistant, light: str):
state = hass.states.get(light)
- supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
- supported = {
- key for key, value in _SUPPORT_OPTS.items() if supported_features & value
+ 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())
- if COLOR_MODE_RGB in supported_color_modes:
- supported.add("color")
+ 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.add("brightness")
- if COLOR_MODE_RGBW in supported_color_modes:
- supported.add("color")
- supported.add("brightness") # see above url
- if COLOR_MODE_XY in supported_color_modes:
- supported.add("color")
- supported.add("brightness") # see above url
- if COLOR_MODE_HS in supported_color_modes:
- supported.add("color")
- supported.add("brightness") # see above url
- if COLOR_MODE_COLOR_TEMP in supported_color_modes:
- supported.add("color_temp")
- supported.add("brightness") # see above url
- if COLOR_MODE_BRIGHTNESS in supported_color_modes:
- supported.add("brightness")
- return supported
+ 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(
@@ -1104,12 +1138,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# Build service data.
service_data = {ATTR_ENTITY_ID: light}
- features = _supported_features(self.hass, light)
+ features, supports_colors = _supported_features(self.hass, light)
# Check transition == 0 to fix #378
- if "transition" in features and transition > 0:
+ if ATTR_TRANSITION in features and transition > 0:
service_data[ATTR_TRANSITION] = transition
- if "brightness" in features and adapt_brightness:
+ if ATTR_BRIGHTNESS in features and adapt_brightness:
brightness = round(255 * self._settings["brightness_pct"] / 100)
service_data[ATTR_BRIGHTNESS] = brightness
@@ -1118,19 +1152,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color"
)
if (
- "color_temp" in features
+ ATTR_COLOR_TEMP_KELVIN in features
and adapt_color
- and not (prefer_rgb_color and "color" in features)
- and not (sleep_rgb and "color" in features)
+ and not (prefer_rgb_color and supports_colors)
+ and not (sleep_rgb and supports_colors)
):
_LOGGER.debug("%s: Setting color_temp of light %s", self._name, light)
- attributes = self.hass.states.get(light).attributes
- min_kelvin = attributes["min_color_temp_kelvin"]
- max_kelvin = attributes["max_color_temp_kelvin"]
+ min_kelvin = features[ATTR_MIN_COLOR_TEMP_KELVIN]
+ max_kelvin = features[ATTR_MAX_COLOR_TEMP_KELVIN]
color_temp_kelvin = self._settings["color_temp_kelvin"]
color_temp_kelvin = max(min(color_temp_kelvin, max_kelvin), min_kelvin)
service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin
- elif "color" in features and adapt_color:
+ elif supports_colors and adapt_color:
_LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light)
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
From adf0d0cf308a6cd3a57462d691f335a32665b75d Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Wed, 26 Apr 2023 21:10:52 -0500
Subject: [PATCH 025/549] Auto reload YAML config changes (#573)
* Update __init__.py
* Update __init__.py
* skip installing codecov
* remove in CI
* test 2023.4.6
---------
Co-authored-by: Bas Nijholt
---
.github/workflows/install_dependencies/action.yml | 2 ++
.github/workflows/pytest.yaml | 2 +-
custom_components/adaptive_lighting/__init__.py | 12 +++++++++---
3 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/install_dependencies/action.yml b/.github/workflows/install_dependencies/action.yml
index df0bee8a..3041698b 100644
--- a/.github/workflows/install_dependencies/action.yml
+++ b/.github/workflows/install_dependencies/action.yml
@@ -36,6 +36,8 @@ runs:
run: |
echo "::warning::### WARNING! Deprecation warnings muted with option '--use-pep517' please address this at some point in pytest.yaml. ###"
pip install -r core/requirements.txt --use-pep517
+ # because they decided to pull codecov the package from PyPI...
+ sed -i '/codecov/d' core/requirements_test.txt
pip install -r core/requirements_test.txt --use-pep517
pip install -e core/ --use-pep517
pip install ulid-transform # this is in Adaptive-lighting's manifest.json
diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml
index cea75602..394acef3 100644
--- a/.github/workflows/pytest.yaml
+++ b/.github/workflows/pytest.yaml
@@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
python-version: ["3.10"]
- core-version: ["2023.2.5", "2023.3.6", "2023.4.0", "dev"]
+ core-version: ["2023.2.5", "2023.3.6", "2023.4.6", "dev"]
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3
diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py
index dc928a6b..07a0a758 100755
--- a/custom_components/adaptive_lighting/__init__.py
+++ b/custom_components/adaptive_lighting/__init__.py
@@ -6,7 +6,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_SOURCE
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.reload import async_setup_reload_service
import voluptuous as vol
from .const import (
@@ -36,10 +35,13 @@ CONFIG_SCHEMA = vol.Schema(
)
+async def reload_configuration_yaml(event: dict, hass: HomeAssistant):
+ """Reload configuration.yaml."""
+ await hass.services.async_call("homeassistant", "check_config", {})
+
+
async def async_setup(hass: HomeAssistant, config: dict[str, Any]):
"""Import integration from config."""
- # This will reload any changes the user made to any YAML configurations.
- await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
if DOMAIN in config:
for entry in config[DOMAIN]:
@@ -55,6 +57,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Set up the component."""
data = hass.data.setdefault(DOMAIN, {})
+ # This will reload any changes the user made to any YAML configurations.
+ # Called during 'quick reload' or hass.reload_config_entry
+ hass.bus.async_listen("hass.config.entry_updated", reload_configuration_yaml)
+
undo_listener = config_entry.add_update_listener(async_update_options)
data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener}
for platform in PLATFORMS:
From 47c5ea93e0051c88d728df9ab289dc25df74fcd2 Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Thu, 27 Apr 2023 14:56:49 -0500
Subject: [PATCH 026/549] Add `test_supported_features` and fix the problem
introduced in #565 (#575)
* Update test_switch.py
* Update test_switch.py
* test is now done.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* fix the test only.
test is backwards compatible with the old method.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* fix _supported_to_attributes
everything works now.
* pre-commit fixes
cannot fix the `function too complex` problem.
* ignore test_switch.py in `pre-commit-config.yaml`
* Add ignore C901 to test_supported_features
* remove commented out code
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt
Co-authored-by: Bas Nijholt
---
custom_components/adaptive_lighting/switch.py | 31 +++----
tests/test_switch.py | 84 ++++++++++++++++++-
2 files changed, 100 insertions(+), 15 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 3eabf7ff..f933f068 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -155,12 +155,13 @@ from .const import (
)
_SUPPORT_OPTS = {
- "brightness": SUPPORT_BRIGHTNESS,
- "color_temp": SUPPORT_COLOR_TEMP,
- "color": SUPPORT_COLOR,
- "transition": SUPPORT_TRANSITION,
+ COLOR_MODE_BRIGHTNESS: SUPPORT_BRIGHTNESS,
+ COLOR_MODE_COLOR_TEMP: SUPPORT_COLOR_TEMP,
+ CONST_COLOR: SUPPORT_COLOR,
+ ATTR_TRANSITION: SUPPORT_TRANSITION,
}
+
VALID_COLOR_MODES = {
COLOR_MODE_BRIGHTNESS: ATTR_BRIGHTNESS,
COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP_KELVIN,
@@ -642,16 +643,18 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
def _supported_to_attributes(supported):
supported_attributes = {}
supports_colors = False
- for mode, attr in VALID_COLOR_MODES.items():
- if mode not in supported:
- continue
- supported_attributes[attr] = True
- if (
- not supports_colors
- and mode != COLOR_MODE_BRIGHTNESS
- and mode != COLOR_MODE_COLOR_TEMP
- ):
- supports_colors = True
+ for mode in supported:
+ attr = VALID_COLOR_MODES.get(mode)
+ if attr:
+ supported_attributes[attr] = True
+ if attr in COLOR_ATTRS:
+ supports_colors = True
+ # ATTR_SUPPORTED_FEATURES only
+ elif mode in _SUPPORT_OPTS:
+ supported_attributes[mode] = True
+ if CONST_COLOR in supported_attributes:
+ supports_colors = True
+ supported_attributes.pop(CONST_COLOR)
return supported_attributes, supports_colors
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 476e83cc..884b796b 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -3,9 +3,10 @@
import asyncio
from copy import deepcopy
import datetime
+import itertools
import logging
from random import randint
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
from homeassistant.components.adaptive_lighting.const import (
ADAPT_BRIGHTNESS_SWITCH,
@@ -25,6 +26,7 @@ from homeassistant.components.adaptive_lighting.const import (
CONF_TRANSITION,
CONF_TURN_ON_LIGHTS,
CONF_USE_DEFAULTS,
+ CONST_COLOR,
DEFAULT_MAX_BRIGHTNESS,
DEFAULT_NAME,
DEFAULT_SLEEP_BRIGHTNESS,
@@ -37,7 +39,10 @@ from homeassistant.components.adaptive_lighting.const import (
UNDO_UPDATE_LISTENER,
)
from homeassistant.components.adaptive_lighting.switch import (
+ _SUPPORT_OPTS,
+ VALID_COLOR_MODES,
_attributes_have_changed,
+ _supported_features,
color_difference_redmean,
create_context,
is_our_context,
@@ -47,9 +52,13 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_TEMP_KELVIN,
+ ATTR_MAX_COLOR_TEMP_KELVIN,
+ ATTR_MIN_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
+ ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
ATTR_XY_COLOR,
+ COLOR_MODE_BRIGHTNESS,
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.light import SERVICE_TURN_OFF
@@ -515,6 +524,79 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass):
assert light not in switch.turn_on_off_listener.lights
+def test_supported_features(hass): # noqa: C901
+ """Test the supported features of a light."""
+
+ possible_legacy_features = {}
+ MAX_COMBINATIONS = 4 # maximum number of elements that can be combined
+ for i in range(1, min(MAX_COMBINATIONS, len(_SUPPORT_OPTS)) + 1):
+ for combination in itertools.combinations(_SUPPORT_OPTS.keys(), i):
+ key = "_".join(combination)
+ value = [v for k, v in _SUPPORT_OPTS.items() if k in combination]
+ possible_legacy_features[key] = value
+
+ possible_color_modes = {}
+ for i in range(1, len(VALID_COLOR_MODES) + 1):
+ for combination in itertools.combinations(VALID_COLOR_MODES.keys(), i):
+ key = "_".join(combination)
+ value = [v for k, v in VALID_COLOR_MODES.items() if k in combination]
+ possible_color_modes[key] = value
+
+ # create a mock HomeAssistant object
+ hass = MagicMock()
+
+ # iterate over possible legacy features
+ for feature_key, feature_values in possible_legacy_features.items():
+ # _LOGGER.debug(feature_values)
+ # set the attributes of the mock state object to the possible legacy feature values
+ state_attrs = {ATTR_SUPPORTED_FEATURES: sum(feature_values)}
+ hass.states.get.return_value.attributes = state_attrs
+
+ # iterate over possible color modes
+ for mode_key, mode_values in possible_color_modes.items():
+ # _LOGGER.debug(mode_values)
+ # set the attributes of the mock state object to the possible color mode values
+ state_attrs[ATTR_SUPPORTED_COLOR_MODES] = set(mode_values)
+ hass.states.get.return_value.attributes = state_attrs
+
+ # Handle both the new and the old _supported_features.
+ result = _supported_features(hass, ENTITY_LIGHT)
+ supported, supports_colors = (
+ result if isinstance(result, tuple) else (result, None)
+ )
+ expected_supported = {} if supports_colors is not None else set()
+ for mode, attr in VALID_COLOR_MODES.items():
+ if mode in mode_values:
+ if supports_colors is None:
+ expected_supported.add(mode)
+ else:
+ expected_supported[attr] = True
+ if supports_colors is True:
+ expected_supported[COLOR_MODE_BRIGHTNESS] = True
+ for opt, value in _SUPPORT_OPTS.items():
+ if value in feature_values:
+ if supports_colors is None:
+ expected_supported.add(opt)
+ else:
+ if supports_colors is True:
+ expected_supported[COLOR_MODE_BRIGHTNESS] = True
+ if opt in VALID_COLOR_MODES:
+ expected_supported[VALID_COLOR_MODES[opt]] = True
+ elif opt != CONST_COLOR:
+ expected_supported[opt] = True
+ if ATTR_MIN_COLOR_TEMP_KELVIN in supported:
+ supported.pop(ATTR_MIN_COLOR_TEMP_KELVIN)
+ if ATTR_MAX_COLOR_TEMP_KELVIN in supported:
+ supported.pop(ATTR_MAX_COLOR_TEMP_KELVIN)
+ assert supported == expected_supported, (
+ f"\nExpected supported: {expected_supported}\n"
+ f"Actual supported: {supported}\n"
+ f"feature_values: {feature_values}\n"
+ f"mode_values: {mode_values}\n"
+ f"supports_colors: {supports_colors}\n"
+ )
+
+
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_manual_control(hass):
"""Test the 'manual control' tracking."""
From 9caf64509349958a340f8cef9207fdb0ea35dbef Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 1 May 2023 16:56:03 -0700
Subject: [PATCH 027/549] [pre-commit.ci] pre-commit autoupdate (#584)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 462e0dc2..1dbf741d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/asottile/pyupgrade
- rev: v3.3.1
+ rev: v3.3.2
hooks:
- id: pyupgrade
args: ["--py39-plus"]
From 29e54185e3218d0692cd7a3f3be7607f411cc71c Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 12 May 2023 11:36:24 -0700
Subject: [PATCH 028/549] Add "See also" to README (#591)
* Add "See also" section
* Add date
* chore(docs): update TOC
* Add YouTube videos
* Add components.cloud
* verbose loggin
---------
Co-authored-by: basnijholt
---
.github/workflows/pytest.yaml | 3 +-
README.md | 56 +++++++++++++++++++----------------
test_dependencies.py | 1 +
3 files changed, 34 insertions(+), 26 deletions(-)
diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml
index 394acef3..4973849f 100644
--- a/.github/workflows/pytest.yaml
+++ b/.github/workflows/pytest.yaml
@@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
python-version: ["3.10"]
- core-version: ["2023.2.5", "2023.3.6", "2023.4.6", "dev"]
+ core-version: ["2023.2.5", "2023.3.6", "2023.4.6", "2023.5.2", "dev"]
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3
@@ -54,6 +54,7 @@ jobs:
run: |
cd core
python3 -X dev -m pytest \
+ -vvv \
-qq \
--timeout=9 \
--durations=10 \
diff --git a/README.md b/README.md
index 9ba7cc03..d1895b91 100644
--- a/README.md
+++ b/README.md
@@ -41,27 +41,28 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as
- - [:gear: Configuration](#gear-configuration)
- - [:memo: Options](#memo-options)
- - [:hammer_and_wrench: Services](#hammer_and_wrench-services)
- - [`adaptive_lighting.apply`](#adaptive_lightingapply)
- - [`adaptive_lighting.set_manual_control`](#adaptive_lightingset_manual_control)
- - [`adaptive_lighting.change_switch_settings`](#adaptive_lightingchange_switch_settings)
- - [:robot: Automation examples](#robot-automation-examples)
+- [:gear: Configuration](#gear-configuration)
+ - [:memo: Options](#memo-options)
+ - [:hammer_and_wrench: Services](#hammer_and_wrench-services)
+ - [`adaptive_lighting.apply`](#adaptive_lightingapply)
+ - [`adaptive_lighting.set_manual_control`](#adaptive_lightingset_manual_control)
+ - [`adaptive_lighting.change_switch_settings`](#adaptive_lightingchange_switch_settings)
+- [:robot: Automation examples](#robot-automation-examples)
- [Additional Information](#additional-information)
- [:sos: Troubleshooting](#sos-troubleshooting)
- [:exclamation: Common Problems & Solutions](#exclamation-common-problems--solutions)
- [:bulb: Lights Not Responding or Turning On by Themselves](#bulb-lights-not-responding-or-turning-on-by-themselves)
- - [:signal_strength: WiFi Networks](#signal_strength-wifi-networks)
- - [:spider_web: Zigbee, Z-Wave, and Other Mesh Networks](#spider_web-zigbee-z-wave-and-other-mesh-networks)
+ - [:signal_strength: WiFi Networks](#signal_strength-wifi-networks)
+ - [:spider_web: Zigbee, Z-Wave, and Other Mesh Networks](#spider_web-zigbee-z-wave-and-other-mesh-networks)
- [:rainbow: Light Colors Not Matching](#rainbow-light-colors-not-matching)
- [:bulb: Bulb-Specific Issues](#bulb-bulb-specific-issues)
- - [:bar_chart: Graphs!](#bar_chart-graphs)
- - [:sunny: Sun Position](#sunny-sun-position)
- - [:thermometer: Color Temperature](#thermometer-color-temperature)
- - [:high_brightness: Brightness](#high_brightness-brightness)
- - [While using `transition_until_sleep: true`](#while-using-transition_until_sleep-true)
- - [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors)
+- [:bar_chart: Graphs!](#bar_chart-graphs)
+ - [:sunny: Sun Position](#sunny-sun-position)
+ - [:thermometer: Color Temperature](#thermometer-color-temperature)
+ - [:high_brightness: Brightness](#high_brightness-brightness)
+ - [While using `transition_until_sleep: true`](#while-using-transition_until_sleep-true)
+- [:eyes: See also](#eyes-see-also)
+- [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors)
@@ -310,13 +311,13 @@ iphone_carly_wakeup:
-# Additional Information
+## Additional Information
For more details on adding the integration and setting options, refer to the [documentation of the PR](https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/) and [this video tutorial on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/).
Adaptive Lighting was initially inspired by @claytonjn's [hass-circadian\_lighting](https://github.com/claytonjn/hass-circadian_lighting), but has since been entirely rewritten and expanded with new features.
-# :sos: Troubleshooting
+## :sos: Troubleshooting
Encountering issues? Enable debug logging in your `configuration.yaml`:
@@ -329,9 +330,9 @@ logger:
After the issue occurs, create a new issue report with the log (`/config/home-assistant.log`).
-## :exclamation: Common Problems & Solutions
+### :exclamation: Common Problems & Solutions
-### :bulb: Lights Not Responding or Turning On by Themselves
+#### :bulb: Lights Not Responding or Turning On by Themselves
Adaptive Lighting sends more commands to lights than a typical human user would. If your light control network is unhealthy, you may experience:
@@ -353,7 +354,7 @@ For most Zigbee networks, **using groups is essential for optimal performance**.
As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group. Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit.
-### :rainbow: Light Colors Not Matching
+#### :rainbow: Light Colors Not Matching
Bulbs from different manufacturers or models may have varying color temperature specifications. For instance, if you have two Adaptive Lighting configurations—one with only Philips Hue White Ambiance bulbs and another with a mix of Philips Hue White Ambiance and Sengled bulbs—the Philips Hue bulbs may appear to have different color temperatures despite having identical settings.
@@ -362,7 +363,7 @@ To resolve this:
1. Include only bulbs of the same make and model in a single Adaptive Lighting configuration.
2. Rearrange bulbs so that different color temperatures are not visible simultaneously.
-### :bulb: Bulb-Specific Issues
+#### :bulb: Bulb-Specific Issues
Certain bulbs may have issues with long light transition commands:
@@ -372,18 +373,23 @@ Certain bulbs may have issues with long light transition commands:
## :bar_chart: Graphs!
These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es).
-#### :sunny: Sun Position
+### :sunny: Sun Position

-#### :thermometer: Color Temperature
+### :thermometer: Color Temperature

-#### :high_brightness: Brightness
+### :high_brightness: Brightness

-#### While using `transition_until_sleep: true`
+### While using `transition_until_sleep: true`

+## :eyes: See also
+
+- [*Sleep better with Adaptive Lighting in Home Assistant*](https://wartner.io/sleep-better-with-adaptive-lightning-in-home-assistant/) by Florian Wartner on 2023-02-23 (blog post 📜)
+- [*Automatic smart light brightness and color based on the sun*](https://www.youtube.com/watch?v=Rg3zI1Oyk3c) by Home Automation Guy on 2022-08-31 (YouTube video 📺)
+- [*Adaptive Lighting Blew My Mind in Home Assistant - How to set it up*](https://www.youtube.com/watch?v=c1cnccmgl3k) by Smart Home Junkie on 2022-06-26 (YouTube video 📺)
## :busts_in_silhouette: Contributors
diff --git a/test_dependencies.py b/test_dependencies.py
index a4dc0e89..58b4718f 100644
--- a/test_dependencies.py
+++ b/test_dependencies.py
@@ -25,6 +25,7 @@ required = [
"components.http",
"components.stream",
"components.conversation",
+ "components.cloud",
]
to_install = []
for r in required:
From 2033fcebe9d6edb0c792f0c7c505d21362629599 Mon Sep 17 00:00:00 2001
From: Mario Guggenberger
Date: Sun, 21 May 2023 19:36:16 +0200
Subject: [PATCH 029/549] fix: lights unexpectedly turn back on after
switch-off (#590)
---
custom_components/adaptive_lighting/switch.py | 23 +++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index f933f068..9ed3e0da 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1111,7 +1111,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context=self.create_context("interval"),
)
- async def _adapt_light(
+ async def _adapt_light( # noqa: C901
self,
light: str,
transition: int | None = None,
@@ -1200,9 +1200,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context=context,
)
- if not self._separate_turn_on_commands:
- await turn_on(service_data)
- else:
+ async def turn_on_split():
# Could be a list of length 1 or 2
service_datas = _split_service_data(
service_data, adapt_brightness, adapt_color
@@ -1215,6 +1213,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
await asyncio.sleep(self._send_split_delay / 1000.0)
await turn_on(service_datas[1])
+ if not self._separate_turn_on_commands:
+ await turn_on(service_data)
+ else:
+ split_tasks = self.turn_on_off_listener.split_adaptation_tasks
+ if (previous_task := split_tasks.get(light)) is not None:
+ previous_task.cancel()
+ try:
+ split_tasks[light] = asyncio.ensure_future(turn_on_split())
+ await split_tasks[light]
+ except asyncio.CancelledError:
+ _LOGGER.debug("Split adaptation of %s cancelled", light)
+
async def _update_attrs_and_maybe_adapt_lights(
self,
lights: list[str] | None = None,
@@ -1685,6 +1695,8 @@ class TurnOnOffListener:
self.last_state_change: dict[str, list[State]] = {}
# Track last 'service_data' to 'light.turn_on' resulting from this integration
self.last_service_data: dict[str, dict[str, Any]] = {}
+ # Track ongoing split adaptations to be able to cancel them
+ self.split_adaptation_tasks: dict[str, asyncio.Task] = {}
# Track auto reset of manual_control
self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {}
@@ -1801,6 +1813,9 @@ class TurnOnOffListener:
self.last_state_change.pop(light, None)
self.last_service_data.pop(light, None)
+ if (task := self.split_adaptation_tasks.get(light)) is not None:
+ task.cancel()
+
async def turn_on_off_event_listener(self, event: Event) -> None:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
domain = event.data.get(ATTR_DOMAIN)
From bf3b709ba62d57f70ff5d04c9416cb35b1bd7eb9 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Sun, 21 May 2023 13:35:45 -0700
Subject: [PATCH 030/549] docs: add protyposis as a contributor for code (#597)
---
.all-contributorsrc | 9 +++++++++
README.md | 4 +++-
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 6c825dad..33b875f2 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -440,6 +440,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "protyposis",
+ "name": "Mario Guggenberger",
+ "avatar_url": "https://avatars.githubusercontent.com/u/189372?v=4",
+ "profile": "http://protyposis.net",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index d1895b91..1a0830b0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -429,6 +429,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting
 Hudson Brendon 🌍 |
 Gabriel Visser 📖 |
 Gleb 🌍 |
+  Deleted user 🌍 |
 Avi Miller 📖 💻 |
 Denys Dovhan 🌍 |
 David Stenbeck 📖 |
@@ -457,6 +458,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting
 Chris 💻 |
 Raman Gupta 💻 |
 igiannakas 💻 |
+  Mario Guggenberger 💻 |
From 4683f083f69952af7e9c26f9064305a62eddfe1a Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 22 May 2023 12:16:40 -0700
Subject: [PATCH 031/549] Update version to 1.12.0 in manifest.json (#596)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 2dec4c83..85aa0c08 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.11.0"
+ "version": "1.12.0"
}
From 2c8a45604ad070b4f67edb10c07a70b6b01318ff Mon Sep 17 00:00:00 2001
From: Mario Guggenberger
Date: Fri, 9 Jun 2023 01:01:19 +0200
Subject: [PATCH 032/549] feat: brightness prioritization (#598)
* feat: optional brightness prioritization
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Remove config flag and change default split order
* Fix test
* Fix edge case
* Add tests
* Backwards compatiblity
* Fix another edge case
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
custom_components/adaptive_lighting/switch.py | 122 +++++++++++-------
tests/test_switch.py | 113 +++++++++++++++-
2 files changed, 188 insertions(+), 47 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 9ed3e0da..32d3661a 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -202,6 +202,8 @@ BRIGHTNESS_ATTRS = {
# Keep a short domain version for the context instances (which can only be 36 chars)
_DOMAIN_SHORT = "al"
+ServiceData = dict[str, Any]
+
def _int_to_base36(num: int) -> str:
"""
@@ -280,25 +282,39 @@ def is_our_context(context: Context | None) -> bool:
return f":{_DOMAIN_SHORT}:" in context.id
-def _split_service_data(service_data, adapt_brightness, adapt_color):
- """Split service_data into two dictionaries (for color and brightness)."""
- transition = service_data.get(ATTR_TRANSITION)
- if transition is not None:
- # Split the transition over both commands
- service_data[ATTR_TRANSITION] /= 2
- service_datas = []
- if adapt_color:
- service_data_color = service_data.copy()
- service_data_color.pop(ATTR_BRIGHTNESS, None)
- service_datas.append(service_data_color)
- if adapt_brightness:
- service_data_brightness = service_data.copy()
- service_data_brightness.pop(ATTR_RGB_COLOR, None)
- service_data_brightness.pop(ATTR_COLOR_TEMP_KELVIN, None)
- service_datas.append(service_data_brightness)
+def _prepare_service_calls(service_data: ServiceData, split=False) -> list[ServiceData]:
+ """Prepares the service data for service calls.
- if not service_datas: # neither adapt_brightness nor adapt_color
+ Processes the service_data according to the config flags, optionally splitting
+ it into multiple data items for the separate adaptation of different attributes.
+ Returns a list of service_datas that indicates the required service calls. If
+ no splitting is necessary, the output is a list with a single item.
+ """
+ if not split:
return [service_data]
+
+ common_attrs = {ATTR_ENTITY_ID}
+ common_data = {k: service_data[k] for k in common_attrs if k in service_data}
+
+ attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS]
+ service_datas = []
+
+ for attributes in attributes_split_sequence:
+ split_data = {
+ attribute: service_data[attribute]
+ for attribute in attributes
+ if service_data.get(attribute)
+ }
+ if split_data:
+ service_datas.append(common_data | split_data)
+
+ # Distribute the transition duration across all service calls
+ if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None:
+ transition = service_data[ATTR_TRANSITION] / len(service_datas)
+
+ for service_data in service_datas:
+ service_data[ATTR_TRANSITION] = transition
+
return service_datas
@@ -1185,7 +1201,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
else:
self.turn_on_off_listener.last_service_data[light] = service_data
- async def turn_on(service_data):
+ service_datas = _prepare_service_calls(
+ service_data, self._separate_turn_on_commands
+ )
+ await self._make_cancellable_adaptation_calls(service_datas, context, light)
+
+ async def _make_adaptation_calls(
+ self, service_datas: list[ServiceData], context: Context
+ ):
+ """Executes a sequence of adaptation service calls for the given service datas."""
+ for i, service_data in enumerate(service_datas):
+ is_first_call = i == 0
+
+ # Sleep _between_ multiple service calls, but not before the first or a single one.
+ if not is_first_call:
+ await asyncio.sleep(service_data.get(ATTR_TRANSITION, 0))
+ await asyncio.sleep(self._send_split_delay / 1000.0)
+
_LOGGER.debug(
"%s: Scheduling 'light.turn_on' with the following 'service_data': %s"
" with context.id='%s'",
@@ -1200,30 +1232,27 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context=context,
)
- async def turn_on_split():
- # Could be a list of length 1 or 2
- service_datas = _split_service_data(
- service_data, adapt_brightness, adapt_color
- )
- await turn_on(service_datas[0])
- if len(service_datas) == 2:
- transition = service_datas[0].get(ATTR_TRANSITION)
- if transition is not None:
- await asyncio.sleep(transition)
- await asyncio.sleep(self._send_split_delay / 1000.0)
- await turn_on(service_datas[1])
+ async def _make_cancellable_adaptation_calls(
+ self, service_datas: list[ServiceData], context: Context, light_id: str
+ ):
+ """Executes a cancellable sequence of adaptation service calls for the given service datas.
- if not self._separate_turn_on_commands:
- await turn_on(service_data)
- else:
- split_tasks = self.turn_on_off_listener.split_adaptation_tasks
- if (previous_task := split_tasks.get(light)) is not None:
- previous_task.cancel()
- try:
- split_tasks[light] = asyncio.ensure_future(turn_on_split())
- await split_tasks[light]
- except asyncio.CancelledError:
- _LOGGER.debug("Split adaptation of %s cancelled", light)
+ Wraps the sequence of service calls in a task that can be cancelled from elsewhere, e.g.,
+ to cancel an ongoing adaptation when a light is turned off.
+ """
+ # Prevent overlap of multiple adaptation sequences
+ self.turn_on_off_listener.cancel_ongoing_adaptation_calls(light_id)
+
+ # Execute adaptation calls within a task
+ try:
+ task = self.turn_on_off_listener.adaptation_tasks[
+ light_id
+ ] = asyncio.ensure_future(
+ self._make_adaptation_calls(service_datas, context)
+ )
+ await task
+ except asyncio.CancelledError:
+ _LOGGER.debug("Ongoing adaptation of %s cancelled", light_id)
async def _update_attrs_and_maybe_adapt_lights(
self,
@@ -1696,7 +1725,7 @@ class TurnOnOffListener:
# Track last 'service_data' to 'light.turn_on' resulting from this integration
self.last_service_data: dict[str, dict[str, Any]] = {}
# Track ongoing split adaptations to be able to cancel them
- self.split_adaptation_tasks: dict[str, asyncio.Task] = {}
+ self.adaptation_tasks: dict[str, asyncio.Task] = {}
# Track auto reset of manual_control
self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {}
@@ -1802,6 +1831,11 @@ class TurnOnOffListener:
self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset)
+ def cancel_ongoing_adaptation_calls(self, light_id: str):
+ """Cancels an ongoing sequence of adaptation service calls for a specific light entity."""
+ if (previous_task := self.adaptation_tasks.get(light_id)) is not None:
+ previous_task.cancel()
+
def reset(self, *lights, reset_manual_control=True) -> None:
"""Reset the 'manual_control' status of the lights."""
for light in lights:
@@ -1812,9 +1846,7 @@ class TurnOnOffListener:
timer.cancel()
self.last_state_change.pop(light, None)
self.last_service_data.pop(light, None)
-
- if (task := self.split_adaptation_tasks.get(light)) is not None:
- task.cancel()
+ self.cancel_ongoing_adaptation_calls(light)
async def turn_on_off_event_listener(self, event: Event) -> None:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 884b796b..0b0826dc 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -42,6 +42,7 @@ from homeassistant.components.adaptive_lighting.switch import (
_SUPPORT_OPTS,
VALID_COLOR_MODES,
_attributes_have_changed,
+ _prepare_service_calls,
_supported_features,
color_difference_redmean,
create_context,
@@ -777,8 +778,8 @@ async def test_auto_reset_manual_control(hass):
assert (
switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] > 0
)
- await asyncio.sleep(0.3) # Should be enough time for auto reset
await update()
+ await asyncio.sleep(0.3) # Should be enough time for auto reset
assert not manual_control[light.entity_id], (light, manual_control)
assert (
light.entity_id not in switch.extra_state_attributes["autoreset_time_remaining"]
@@ -791,8 +792,8 @@ async def test_auto_reset_manual_control(hass):
await asyncio.sleep(0.05) # Less than 0.1
assert manual_control[light.entity_id]
- await asyncio.sleep(0.3) # Wait the auto reset time
await update()
+ await asyncio.sleep(0.3) # Wait the auto reset time
assert not manual_control[light.entity_id]
@@ -1385,3 +1386,111 @@ async def test_change_switch_settings_service(hass):
# testing with "configuration" should revert back to 2500
await change_switch_settings(**{CONF_USE_DEFAULTS: "configuration"})
assert switch._sun_light_settings.min_color_temp == 2500
+
+
+@pytest.mark.parametrize(
+ "service_data_input,split,service_data_expected",
+ [
+ (
+ {"foo": 1, ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
+ False,
+ [{"foo": 1, ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}],
+ ),
+ (
+ {"foo": 1},
+ True,
+ [],
+ ),
+ (
+ {ATTR_BRIGHTNESS: 10},
+ True,
+ [{ATTR_BRIGHTNESS: 10}],
+ ),
+ (
+ {ATTR_COLOR_TEMP_KELVIN: 3500},
+ True,
+ [{ATTR_COLOR_TEMP_KELVIN: 3500}],
+ ),
+ (
+ {ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10},
+ True,
+ [{ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}],
+ ),
+ (
+ {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500},
+ True,
+ [{ATTR_BRIGHTNESS: 10}, {ATTR_COLOR_TEMP_KELVIN: 3500}],
+ ),
+ (
+ {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 2},
+ True,
+ [
+ {ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 1},
+ {ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 1},
+ ],
+ ),
+ (
+ {ATTR_TRANSITION: 1},
+ True,
+ [],
+ ),
+ ],
+ ids=[
+ "pass through when splitting is disabled",
+ "remove irrelevant attributes",
+ "brightness only yields one service call",
+ "color only yields one service call",
+ "include entity ID",
+ "brightness and color are split into two with brightness first",
+ "transition time is distributed among service calls",
+ "ignore transition time without service calls",
+ ],
+)
+async def test_prepare_service_calls(service_data_input, split, service_data_expected):
+ """Test the preparation of service calls, e.g., splitting."""
+ assert _prepare_service_calls(service_data_input, split) == service_data_expected
+
+
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
+async def test_cancellable_service_calls_task(hass):
+ """Test the creation and execution of the task that wraps adaptation service calls."""
+ (light, *_) = await setup_lights(hass)
+ _, switch = await setup_switch(hass, {CONF_SEPARATE_TURN_ON_COMMANDS: True})
+ context = switch.create_context("test")
+
+ assert switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) is None
+
+ await switch._make_cancellable_adaptation_calls(
+ [
+ {
+ ATTR_BRIGHTNESS: 10,
+ ATTR_COLOR_TEMP_KELVIN: 10,
+ ATTR_ENTITY_ID: light.entity_id,
+ }
+ ],
+ context,
+ light.entity_id,
+ )
+
+ task = switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id)
+ assert task is not None
+ assert task.done()
+
+
+@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
+async def test_service_calls_task_cancellation(hass):
+ """Tests if the task that wraps ongoing adaptation service calls gets cancelled."""
+ _, switch = await setup_switch(hass, {})
+ entity_id = "test_id"
+
+ task = asyncio.ensure_future(asyncio.sleep(1))
+ switch.turn_on_off_listener.adaptation_tasks[entity_id] = task
+
+ switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(entity_id)
+
+ try:
+ await task
+ except asyncio.CancelledError:
+ pass
+
+ assert task.cancelled()
From 90b8d6089d74b401c8708d5828489fd2f16c5f4a Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 8 Jun 2023 16:09:57 -0700
Subject: [PATCH 033/549] Bump to 1.13.0 in manifest.json (#610)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 85aa0c08..a5e514cb 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.12.0"
+ "version": "1.13.0"
}
From 12dae4b6a65b4705c5ec32bd05c97053bc4150ac Mon Sep 17 00:00:00 2001
From: Mario Guggenberger
Date: Sun, 11 Jun 2023 23:12:37 +0200
Subject: [PATCH 034/549] fix: 2023.6 compatibility (#607)
* fix: 2023.6.0 compatibility
* ci: test with Python 3.11 against 2023.6.0b4
* Set 2023.06.0
* Bump to 2023.06.1
---------
Co-authored-by: Bas Nijholt
---
.github/workflows/pytest.yaml | 15 +++++++++++++--
custom_components/adaptive_lighting/switch.py | 2 +-
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml
index 4973849f..1b51f257 100644
--- a/.github/workflows/pytest.yaml
+++ b/.github/workflows/pytest.yaml
@@ -13,8 +13,19 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: ["3.10"]
- core-version: ["2023.2.5", "2023.3.6", "2023.4.6", "2023.5.2", "dev"]
+ include:
+ - python-version: "3.10"
+ core-version: "2023.2.5"
+ - python-version: "3.10"
+ core-version: "2023.3.6"
+ - python-version: "3.10"
+ core-version: "2023.4.6"
+ - python-version: "3.10"
+ core-version: "2023.5.2"
+ - python-version: "3.11"
+ core-version: "2023.6.1"
+ - python-version: "3.11"
+ core-version: "dev"
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 32d3661a..4d36fce1 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -502,7 +502,7 @@ async def async_setup_entry(
data[config_entry.entry_id][SWITCH_DOMAIN] = switch
async_add_entities(
- [switch, sleep_mode_switch, adapt_color_switch, adapt_brightness_switch],
+ [sleep_mode_switch, adapt_color_switch, adapt_brightness_switch, switch],
update_before_add=True,
)
From 6283158ff730644f76d8007c70edbf0ffd6846fe Mon Sep 17 00:00:00 2001
From: Mario Guggenberger
Date: Mon, 12 Jun 2023 00:33:07 +0200
Subject: [PATCH 035/549] VS Code Dev Container (dev & test environment) (#605)
* build: dev container
Add a VS Code Dev Container from the blueprint at https://github.com/ludeeus/integration_blueprint/tree/bceaae212fefefae84d9529cde5cb6f4b60cc865
* build: dev container test setup
Add support for unit testing in the dev container environment with debugging and code coverage.
* ci: adjust to dev container test restructuring
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* ci: fix coverage collection
* build: add dummy light to HA config
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* build: update dev container to Python 3.11 (for HA 2023.6)
* Add VS Code tasks
* Use pre-commit hooks for linting
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Unpin HA version
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt
---
.devcontainer.json | 42 ++++++++
.gitattributes | 1 +
.github/workflows/pytest.yaml | 10 +-
.gitignore | 8 ++
.ruff.toml | 48 +++++++++
.vscode/tasks.json | 17 +++
Dockerfile | 14 ++-
config/configuration.yaml | 19 ++++
requirements.txt | 10 ++
scripts/develop | 20 ++++
scripts/lint | 7 ++
scripts/setup | 8 ++
setup.cfg | 4 +
tests/README.md | 2 +-
tests/conftest.py | 19 ++++
tests/test_config_flow.py | 10 +-
tests/test_init.py | 9 +-
tests/test_switch.py | 192 ++++++++++++++++++----------------
18 files changed, 329 insertions(+), 111 deletions(-)
create mode 100644 .devcontainer.json
create mode 100644 .gitattributes
create mode 100644 .ruff.toml
create mode 100644 .vscode/tasks.json
create mode 100644 config/configuration.yaml
create mode 100644 requirements.txt
create mode 100644 scripts/develop
create mode 100644 scripts/lint
create mode 100644 scripts/setup
create mode 100644 tests/conftest.py
diff --git a/.devcontainer.json b/.devcontainer.json
new file mode 100644
index 00000000..d842d1dd
--- /dev/null
+++ b/.devcontainer.json
@@ -0,0 +1,42 @@
+{
+ "name": "basnijholt/adaptive_lighting",
+ "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.11-bullseye",
+ "postCreateCommand": "scripts/setup",
+ "forwardPorts": [
+ 8123
+ ],
+ "portsAttributes": {
+ "8123": {
+ "label": "Home Assistant",
+ "onAutoForward": "notify"
+ }
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-python.python",
+ "github.vscode-pull-request-github",
+ "ryanluker.vscode-coverage-gutters",
+ "ms-python.vscode-pylance"
+ ],
+ "settings": {
+ "files.eol": "\n",
+ "editor.tabSize": 4,
+ "python.pythonPath": "/usr/bin/python3",
+ "python.analysis.autoSearchPaths": false,
+ "python.linting.pylintEnabled": true,
+ "python.linting.enabled": true,
+ "python.formatting.provider": "black",
+ "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
+ "editor.formatOnPaste": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "files.trimTrailingWhitespace": true
+ }
+ }
+ },
+ "remoteUser": "vscode",
+ "features": {
+ "ghcr.io/devcontainers/features/rust:1": {}
+ }
+}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..6313b56c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml
index 1b51f257..b5089ded 100644
--- a/.github/workflows/pytest.yaml
+++ b/.github/workflows/pytest.yaml
@@ -50,11 +50,6 @@ jobs:
run: |
cd core
- # Link homeassitant.components.adaptive_lighting
- cd homeassistant/components
- ln -fs ../../../custom_components/adaptive_lighting adaptive_lighting
- cd -
-
# Link adaptive_lighting tests
cd tests/components/
ln -fs ../../../tests adaptive_lighting
@@ -63,14 +58,17 @@ jobs:
- name: Run pytest
timeout-minutes: 60
run: |
+ export PYTHONPATH=${PYTHONPATH}:${PWD}
cd core
python3 -X dev -m pytest \
-vvv \
-qq \
--timeout=9 \
--durations=10 \
- --cov="homeassistant" \
+ --cov="custom_components.adaptive_lighting" \
--cov-report=xml \
-o console_output_style=count \
-p no:sugar \
tests/components/adaptive_lighting
+ env:
+ HA_CLONE: true
diff --git a/.gitignore b/.gitignore
index b6e47617..eeef9022 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,11 @@ dmypy.json
# Pyre type checker
.pyre/
+
+# IDEs
+.vscode
+.idea
+
+# Home Assistant configuration
+config/*
+!config/configuration.yaml
diff --git a/.ruff.toml b/.ruff.toml
new file mode 100644
index 00000000..260b1883
--- /dev/null
+++ b/.ruff.toml
@@ -0,0 +1,48 @@
+# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml
+
+target-version = "py310"
+
+select = [
+ "B007", # Loop control variable {name} not used within loop body
+ "B014", # Exception handler with duplicate exception
+ "C", # complexity
+ "D", # docstrings
+ "E", # pycodestyle
+ "F", # pyflakes/autoflake
+ "ICN001", # import concentions; {name} should be imported as {asname}
+ "PGH004", # Use specific rule codes when using noqa
+ "PLC0414", # Useless import alias. Import alias does not rename original package.
+ "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass
+ "SIM117", # Merge with-statements that use the same scope
+ "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys()
+ "SIM201", # Use {left} != {right} instead of not {left} == {right}
+ "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a}
+ "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'.
+ "SIM401", # Use get from dict with default instead of an if block
+ "T20", # flake8-print
+ "TRY004", # Prefer TypeError exception for invalid type
+ "RUF006", # Store a reference to the return value of asyncio.create_task
+ "UP", # pyupgrade
+ "W", # pycodestyle
+]
+
+ignore = [
+ "D202", # No blank lines allowed after function docstring
+ "D203", # 1 blank line required before class docstring
+ "D213", # Multi-line docstring summary should start at the second line
+ "D404", # First word of the docstring should not be This
+ "D406", # Section name should end with a newline
+ "D407", # Section name underlining
+ "D411", # Missing blank line before section
+ "E501", # line too long
+ "E731", # do not assign a lambda expression, use a def
+]
+
+[flake8-pytest-style]
+fixture-parentheses = false
+
+[pyupgrade]
+keep-runtime-typing = true
+
+[mccabe]
+max-complexity = 25
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 00000000..cd2130b2
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,17 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "Run Home Assistant on port 8123",
+ "type": "shell",
+ "command": "scripts/develop",
+ "problemMatcher": []
+ },
+ {
+ "label": "Lint (run pre-commit hooks)",
+ "type": "shell",
+ "command": "scripts/lint",
+ "problemMatcher": []
+ }
+ ]
+}
diff --git a/Dockerfile b/Dockerfile
index 45522da8..4baac59a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -23,12 +23,11 @@ RUN pip3 install -r /core/requirements.txt --use-pep517 && \
pip3 install -r /core/requirements_test.txt --use-pep517 && \
pip3 install -e /core/ --use-pep517
-# Clone the Adaptive Lighting repository
-RUN git clone https://github.com/basnijholt/adaptive-lighting.git /app
+# Copy the Adaptive Lighting repository
+COPY . /app/
# Setup symlinks in core
-RUN ln -s /app/custom_components/adaptive_lighting /core/homeassistant/components/adaptive_lighting && \
- ln -s /app/tests /core/tests/components/adaptive_lighting && \
+RUN ln -s /app/tests /core/tests/components/adaptive_lighting && \
# For test_dependencies.py
ln -s /core /app/core
@@ -37,6 +36,11 @@ RUN pip3 install $(python3 /app/test_dependencies.py) --use-pep517
WORKDIR /core
+# Make 'custom_components/adaptive_lighting' imports available to tests
+ENV PYTHONPATH="${PYTHONPATH}:/app"
+# Enable testing against HA clone (instead of pytest_homeassistant_custom_component)
+ENV HA_CLONE=true
+
ENTRYPOINT ["python3", \
# Enable Python development mode
"-X", "dev", \
@@ -49,7 +53,7 @@ ENTRYPOINT ["python3", \
# Print the 10 slowest tests
"--durations=10", \
# Measure code coverage for the 'homeassistant' package
- "--cov='homeassistant'", \
+ "--cov=custom_components.adaptive_lighting", \
# Generate an XML report of the code coverage
"--cov-report=xml", \
# Generate an HTML report of the code coverage
diff --git a/config/configuration.yaml b/config/configuration.yaml
new file mode 100644
index 00000000..b9d7d45b
--- /dev/null
+++ b/config/configuration.yaml
@@ -0,0 +1,19 @@
+# https://www.home-assistant.io/integrations/default_config/
+default_config:
+
+# https://www.home-assistant.io/integrations/logger/
+logger:
+ default: info
+ logs:
+ custom_components.adaptive_lighting: debug
+
+light:
+ - platform: template
+ lights:
+ dummylight:
+ friendly_name: "Dummy Light"
+ turn_on:
+ turn_off:
+ set_level:
+ set_temperature:
+ supports_transition_template: "{{ true }}"
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..c560a858
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,10 @@
+colorlog==6.7.0
+pip>=21.0,<23.2
+ruff==0.0.265
+pre-commit
+
+# Install HA and test dependencies (pytest, coverage)
+# To pin the dev container to a specific HA version, set this dependency
+# to the adequate version (add `==`) and rebuild the dev container.
+# See https://github.com/MatthewFlamm/pytest-homeassistant-custom-component/releases for version mappings.
+pytest-homeassistant-custom-component
diff --git a/scripts/develop b/scripts/develop
new file mode 100644
index 00000000..e7ce50cd
--- /dev/null
+++ b/scripts/develop
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+# Create config dir if not present
+if [[ ! -d "${PWD}/config" ]]; then
+ mkdir -p "${PWD}/config"
+ hass --config "${PWD}/config" --script ensure_config
+fi
+
+# Set the path to custom_components
+## This let's us have the structure we want /custom_components/adaptive_lighting
+## while at the same time have Home Assistant configuration inside /config
+## without resulting to symlinks.
+export PYTHONPATH="${PYTHONPATH}:${PWD}/custom_components"
+
+# Start Home Assistant
+hass --config "${PWD}/config" --debug
diff --git a/scripts/lint b/scripts/lint
new file mode 100644
index 00000000..55a1f485
--- /dev/null
+++ b/scripts/lint
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+pre-commit run --all-files
diff --git a/scripts/setup b/scripts/setup
new file mode 100644
index 00000000..0688d70d
--- /dev/null
+++ b/scripts/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+python3 -m pip install --requirement requirements.txt
+pre-commit install-hooks
diff --git a/setup.cfg b/setup.cfg
index 284326f5..a82d84b8 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,3 +9,7 @@ max-complexity = 18
select = B,C,E,F,W,T4,B9
per-file-ignores =
code_example.py: E402, E501
+
+[tool:pytest]
+testpaths = tests
+asyncio_mode = auto
diff --git a/tests/README.md b/tests/README.md
index 1c472541..dd125d97 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,7 +1,7 @@
# Developer notes for the tests directory
To run the tests, check out the [CI configuration](../.github/workflows/pytest.yml) to see how they are executed in the CI pipeline.
-Alternatively, you can use the provided Docker image to run the tests locally.
+Alternatively, you can use the provided Docker image to run the tests locally or run them with VS Code directly in the dev container.
To run the tests using the Docker image, navigate to the `adaptive-lighting` repo folder and execute the following command:
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 00000000..14f7e644
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,19 @@
+"""Fixtures for testing."""
+import os
+import sys
+
+import pytest
+
+# Tests in the dev enviromentment use the pytest_homeassistant_custom_component instead of
+# a cloned HA core repo for a simple and clean structure. To still test against a HA core
+# clone (e.g. the dev branch for which no pytest_homeassistant_custom_component exists
+# because HA does not publish dev snapshot packages), set the HA_CLONE env variable.
+if "HA_CLONE" in os.environ:
+ # Rewire the testing package to the cloned test modules. See the test `Dockerfile`
+ # for setup details.
+ sys.modules["pytest_homeassistant_custom_component"] = __import__("tests")
+
+
+@pytest.fixture(autouse=True)
+def auto_enable_custom_integrations(enable_custom_integrations):
+ yield
diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py
index 53901cf9..22384143 100644
--- a/tests/test_config_flow.py
+++ b/tests/test_config_flow.py
@@ -1,6 +1,10 @@
"""Test Adaptive Lighting config flow."""
from homeassistant import data_entry_flow
-from homeassistant.components.adaptive_lighting.const import (
+from homeassistant.config_entries import SOURCE_IMPORT
+from homeassistant.const import CONF_NAME
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+
+from custom_components.adaptive_lighting.const import (
CONF_SUNRISE_TIME,
CONF_SUNSET_TIME,
DEFAULT_NAME,
@@ -8,10 +12,6 @@ from homeassistant.components.adaptive_lighting.const import (
NONE_STR,
VALIDATION_TUPLES,
)
-from homeassistant.config_entries import SOURCE_IMPORT
-from homeassistant.const import CONF_NAME
-
-from tests.common import MockConfigEntry
DEFAULT_DATA = {key: default for key, default, _ in VALIDATION_TUPLES}
diff --git a/tests/test_init.py b/tests/test_init.py
index b6f48e0b..bb0cf977 100644
--- a/tests/test_init.py
+++ b/tests/test_init.py
@@ -1,14 +1,11 @@
"""Tests for Adaptive Lighting integration."""
-from homeassistant.components import adaptive_lighting
-from homeassistant.components.adaptive_lighting.const import (
- DEFAULT_NAME,
- UNDO_UPDATE_LISTENER,
-)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_NAME
from homeassistant.setup import async_setup_component
+from pytest_homeassistant_custom_component.common import MockConfigEntry
-from tests.common import MockConfigEntry
+from custom_components import adaptive_lighting
+from custom_components.adaptive_lighting.const import DEFAULT_NAME, UNDO_UPDATE_LISTENER
async def test_setup_with_config(hass):
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 0b0826dc..0e46f458 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -8,7 +8,49 @@ import logging
from random import randint
from unittest.mock import MagicMock, patch
-from homeassistant.components.adaptive_lighting.const import (
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_BRIGHTNESS_PCT,
+ ATTR_COLOR_TEMP_KELVIN,
+ ATTR_MAX_COLOR_TEMP_KELVIN,
+ ATTR_MIN_COLOR_TEMP_KELVIN,
+ ATTR_RGB_COLOR,
+ ATTR_SUPPORTED_COLOR_MODES,
+ ATTR_TRANSITION,
+ ATTR_XY_COLOR,
+ COLOR_MODE_BRIGHTNESS,
+)
+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
+import homeassistant.config as config_util
+from homeassistant.config_entries import ConfigEntryState
+from homeassistant.const import (
+ ATTR_AREA_ID,
+ ATTR_ENTITY_ID,
+ ATTR_SUPPORTED_FEATURES,
+ CONF_LIGHTS,
+ CONF_NAME,
+ EVENT_STATE_CHANGED,
+ SERVICE_TURN_ON,
+ STATE_OFF,
+ STATE_ON,
+)
+from homeassistant.core import Context, HomeAssistant, State
+from homeassistant.helpers import entity_registry
+from homeassistant.helpers.entity_platform import async_get_platforms
+from homeassistant.setup import async_setup_component
+from homeassistant.util.color import color_temperature_mired_to_kelvin
+import homeassistant.util.dt as dt_util
+import pytest
+from pytest_homeassistant_custom_component.common import (
+ MockConfigEntry,
+ mock_area_registry,
+)
+import ulid_transform
+import voluptuous.error
+
+from custom_components.adaptive_lighting.const import (
ADAPT_BRIGHTNESS_SWITCH,
ADAPT_COLOR_SWITCH,
ATTR_TURN_ON_OFF_LISTENER,
@@ -38,7 +80,7 @@ from homeassistant.components.adaptive_lighting.const import (
SLEEP_MODE_SWITCH,
UNDO_UPDATE_LISTENER,
)
-from homeassistant.components.adaptive_lighting.switch import (
+from custom_components.adaptive_lighting.switch import (
_SUPPORT_OPTS,
VALID_COLOR_MODES,
_attributes_have_changed,
@@ -48,47 +90,6 @@ from homeassistant.components.adaptive_lighting.switch import (
create_context,
is_our_context,
)
-from homeassistant.components.demo.light import DemoLight
-from homeassistant.components.light import (
- ATTR_BRIGHTNESS,
- ATTR_BRIGHTNESS_PCT,
- ATTR_COLOR_TEMP_KELVIN,
- ATTR_MAX_COLOR_TEMP_KELVIN,
- ATTR_MIN_COLOR_TEMP_KELVIN,
- ATTR_RGB_COLOR,
- ATTR_SUPPORTED_COLOR_MODES,
- ATTR_TRANSITION,
- ATTR_XY_COLOR,
- COLOR_MODE_BRIGHTNESS,
-)
-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
-import homeassistant.config as config_util
-from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import (
- ATTR_AREA_ID,
- ATTR_ENTITY_ID,
- ATTR_SUPPORTED_FEATURES,
- CONF_LIGHTS,
- CONF_NAME,
- CONF_PLATFORM,
- EVENT_STATE_CHANGED,
- SERVICE_TURN_ON,
- STATE_OFF,
- STATE_ON,
-)
-from homeassistant.core import Context, State
-from homeassistant.helpers import entity_registry
-from homeassistant.setup import async_setup_component
-from homeassistant.util.color import color_temperature_mired_to_kelvin
-import homeassistant.util.dt as dt_util
-import pytest
-import ulid_transform
-import voluptuous.error
-
-from tests.common import MockConfigEntry, mock_area_registry
-from tests.components.demo.test_light import ENTITY_LIGHT
_LOGGER = logging.getLogger(__name__)
@@ -113,6 +114,7 @@ LAT_LONG_TZS = [
(32.87336, -117.22743, "US/Pacific"),
]
+ENTITY_LIGHT = "light.bed_light"
_SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}"
ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}"
ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}"
@@ -149,50 +151,60 @@ async def setup_switch(hass, extra_data):
return entry, switch
-async def setup_lights(hass):
- """Set up 3 light entities using the 'test' platform."""
+async def setup_lights(hass: HomeAssistant):
+ """Set up 3 light entities using the 'template' platform."""
await async_setup_component(
- hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}}
+ hass,
+ LIGHT_DOMAIN,
+ {
+ LIGHT_DOMAIN: [
+ {
+ "platform": "template",
+ "lights": {
+ "bed_light": {
+ "friendly_name": "Bed Light",
+ "unique_id": "light_1",
+ "turn_on": None,
+ "turn_off": None,
+ "set_level": None,
+ "set_temperature": None,
+ "set_color": None,
+ },
+ "ceiling_lights": {
+ "friendly_name": "Ceiling Lights",
+ "unique_id": "light_2",
+ "turn_on": None,
+ "turn_off": None,
+ "set_level": None,
+ "set_temperature": None,
+ "set_color": None,
+ },
+ "kitchen_lights": {
+ "friendly_name": "Kitchen Lights",
+ "unique_id": "light_3",
+ "turn_on": None,
+ "turn_off": None,
+ "set_level": None,
+ "set_temperature": None,
+ "set_color": None,
+ },
+ },
+ },
+ ]
+ },
)
- await hass.async_block_till_done()
- platform = getattr(hass.components, "test.light")
- while platform.ENTITIES:
- # Make sure it is empty
- platform.ENTITIES.pop()
- lights = [
- DemoLight(
- unique_id="light_1",
- name="Bed Light",
- state=True,
- ct=200,
- ),
- DemoLight(
- unique_id="light_2",
- name="Ceiling Lights",
- state=True,
- ct=380,
- ),
- DemoLight(
- unique_id="light_3",
- name="Kitchen Lights",
- state=False,
- hs_color=(345, 75),
- ct=240,
- ),
- ]
+ await hass.async_block_till_done()
+ platform = async_get_platforms(hass, "template")
+ lights = list(platform[0].entities.values())
+
+ await lights[0].async_turn_on()
+ await lights[1].async_turn_on()
+
for light in lights:
- light.hass = hass
- slug = light.name.lower().replace(" ", "_")
- light.entity_id = f"light.{slug}"
- await light.async_update_ha_state()
+ light._attr_brightness = 255
+ light._attr_color_temp = 250
- platform.ENTITIES.extend(lights)
- platform.init()
- assert await async_setup_component(
- hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {CONF_PLATFORM: "test"}}
- )
- await hass.async_block_till_done()
assert all(hass.states.get(light.entity_id) is not None for light in lights)
return lights
@@ -646,11 +658,12 @@ async def test_manual_control(hass):
await update()
def increased_brightness():
- return (light._brightness + 100) % 255
+ return (light._attr_brightness + 100) % 255
def increased_color_temp():
return max(
- (light._ct + 100) % light.max_color_temp_kelvin, light.min_color_temp_kelvin
+ (light._attr_color_temp + 100) % light.max_color_temp_kelvin,
+ light.min_color_temp_kelvin,
)
# Nothing is manually controlled
@@ -703,7 +716,9 @@ async def test_manual_control(hass):
color_temperature_mired_to_kelvin(mired_range[0]),
)
ptp_kelvin = kelvin_range[1] - kelvin_range[0]
- await turn_light(True, color_temp_kelvin=(light._ct + 100) % ptp_kelvin)
+ await turn_light(
+ True, color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin
+ )
assert manual_control[ENTITY_LIGHT]
await switch.adapt_brightness_switch.async_turn_on() # turn on again
@@ -805,11 +820,12 @@ async def test_apply_service(hass):
assert entity_id not in switch._lights
def increased_brightness():
- return (light._brightness + 100) % 255
+ return (light._attr_brightness + 100) % 255
def increased_color_temp():
return max(
- (light._ct + 100) % light.max_color_temp_kelvin, light.min_color_temp_kelvin
+ (light._attr_color_temp + 100) % light.max_color_temp_kelvin,
+ light.min_color_temp_kelvin,
)
async def change_light():
@@ -1306,7 +1322,7 @@ async def test_area(hass):
area_registry.async_create("test_area")
entity = entity_registry.async_get(hass).async_get_or_create(
- LIGHT_DOMAIN, "demo", light.unique_id
+ LIGHT_DOMAIN, "template", light.unique_id
)
entity = entity_registry.async_get(hass).async_update_entity(
entity.entity_id, area_id="test_area"
From 68d500bd12bdc4877e1596405690c112bc4f5123 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 12 Jun 2023 16:13:29 -0700
Subject: [PATCH 036/549] Update manifest.json (#613)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index a5e514cb..4bbdd6f3 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.13.0"
+ "version": "1.14.0"
}
From 42edd5eb8f673e399fa15fb4a644b9d49ea2a88e Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 12 Jun 2023 17:43:18 -0700
Subject: [PATCH 037/549] [pre-commit.ci] pre-commit autoupdate (#587)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/asottile/pyupgrade: v3.3.2 → v3.6.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.6.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 1dbf741d..d8c823c4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/asottile/pyupgrade
- rev: v3.3.2
+ rev: v3.6.0
hooks:
- id: pyupgrade
args: ["--py39-plus"]
From 36c656b7958f6a9a637b50800534749038e146ee Mon Sep 17 00:00:00 2001
From: Mario Guggenberger
Date: Wed, 14 Jun 2023 18:39:47 +0200
Subject: [PATCH 038/549] docs: Ikea light config recommendations (#614)
---
README.md | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 1a0830b0..bdfcd0e6 100644
--- a/README.md
+++ b/README.md
@@ -365,10 +365,14 @@ To resolve this:
#### :bulb: Bulb-Specific Issues
-Certain bulbs may have issues with long light transition commands:
+These lights are known to exhibit disadvantageous behaviour due to firmware bugs, insufficient functionality, or hardware limitations:
-- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26): If Adaptive Lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off during that time, it may turn back on after approximately 10 seconds to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs indicating the cause of the light turning on. To fix this, set a much shorter transition time, such as 1 second.
-- Additionally, these bulbs may perform poorly in enclosed "dome" style ceiling lights, particularly when hot. While most LEDs (even non-smart ones) state in the fine print that they do not support working in enclosed fixtures, in practice, more expensive bulbs like Philips Hue generally perform better. To resolve this issue, move the problematic bulbs to open-air fixtures.
+- [Sengled Z01-A19NAE26](https://www.zigbee2mqtt.io/devices/Z01-A19NAE26.html#sengled-z01-a19nae26)
+ - Unexpected turn-ons: If Adaptive Lighting sends a long transition time (like the default 45 seconds), and the bulb is turned off during that time, it may turn back on after approximately 10 seconds to continue the transition command. Since the bulb is turning itself on, there will be no obvious trigger in Home Assistant or other logs indicating the cause of the light turning on. To fix this, set a much shorter `transition` time, such as 1 second.
+ - Heat sensitivity: Additionally, these bulbs may perform poorly in enclosed "dome" style ceiling lights, particularly when hot. While most LEDs (even non-smart ones) state in the fine print that they do not support working in enclosed fixtures, in practice, more expensive bulbs like Philips Hue generally perform better. To resolve this issue, move the problematic bulbs to open-air fixtures.
+- Ikea Tradfri bulbs/drivers (and related Ikea smart light products)
+ - Unsupported simultaneous transition of brightness and color: When receiving such a command, they switch the brightness instantly and only transition the color. To get smooth transitions of both brightness and color, enable `separate_turn_on_commands`.
+ - Unresponsiveness during color transitions: No other commands are processed during an ongoing color transition, e.g., turn-off commands are ignored and lights stay on despite being reported as off to Home Assistant. The default config with long transitions thus results in long periods of unresponsiveness. To work around this, disable transitions by setting `transition` to `0`, and increase the adaptation frequency by setting `interval` to a short time, e.g., `15` seconds, to retain the impression of smooth continuous adaptations. Keeping the `initial_transition` is recommended for a smooth fade-in (lights are usually not turned off momentarily after being turned on, in which case a short period of unresponsiveness is tolerable).
## :bar_chart: Graphs!
These graphs were generated using the values calculated by the Adaptive Lighting sensor/switch(es).
From 72e140e293785410815084d7e8e03249caed4d6c Mon Sep 17 00:00:00 2001
From: Mario Guggenberger
Date: Mon, 3 Jul 2023 00:26:07 +0200
Subject: [PATCH 039/549] feat: skip redundant adaptation commands (#615)
* feat: skip redundant adaptation commands
* update README
* style
* Remove "experimental"
---------
Co-authored-by: Bas Nijholt
Co-authored-by: Bas Nijholt
---
README.md | 63 ++--
.../adaptive_lighting/adaptation_utils.py | 164 +++++++++
custom_components/adaptive_lighting/const.py | 16 +
.../adaptive_lighting/strings.json | 3 +-
custom_components/adaptive_lighting/switch.py | 143 +++-----
.../adaptive_lighting/translations/de.json | 3 +-
.../adaptive_lighting/translations/en.json | 3 +-
tests/test_adaptation_utils.py | 336 ++++++++++++++++++
tests/test_switch.py | 87 +----
9 files changed, 617 insertions(+), 201 deletions(-)
create mode 100644 custom_components/adaptive_lighting/adaptation_utils.py
create mode 100644 tests/test_adaptation_utils.py
diff --git a/README.md b/README.md
index bdfcd0e6..e0ba67fc 100644
--- a/README.md
+++ b/README.md
@@ -91,37 +91,38 @@ The YAML and frontend configuration methods support all of the options listed be
-| Variable name | Description | Default | Type |
-|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
-| `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` |
-| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
-| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
-| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
-| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
-| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
-| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
-| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
-| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
-| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
-| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
-| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
-| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
-| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
-| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
-| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
-| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
-| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
-| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
-| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
-| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
-| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
-| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
-| `send_split_delay` | 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 |
+| Variable name | Description | Default | Type |
+| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------- | :----------------------------------- |
+| `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` |
+| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
+| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
+| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
+| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
+| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
+| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
+| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
+| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
+| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
+| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
+| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
+| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
+| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
+| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
+| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
+| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
+| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
+| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
+| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
+| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
+| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
+| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
+| `send_split_delay` | 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 |
+| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py
new file mode 100644
index 00000000..0cfbc7dd
--- /dev/null
+++ b/custom_components/adaptive_lighting/adaptation_utils.py
@@ -0,0 +1,164 @@
+"""Utility functions for adaptation commands."""
+from collections.abc import AsyncGenerator
+from dataclasses import dataclass
+from typing import Any
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_BRIGHTNESS_PCT,
+ ATTR_BRIGHTNESS_STEP,
+ ATTR_BRIGHTNESS_STEP_PCT,
+ ATTR_COLOR_NAME,
+ ATTR_COLOR_TEMP_KELVIN,
+ ATTR_HS_COLOR,
+ ATTR_RGB_COLOR,
+ ATTR_TRANSITION,
+ ATTR_XY_COLOR,
+)
+from homeassistant.const import ATTR_ENTITY_ID
+from homeassistant.core import Context, HomeAssistant, State
+
+COLOR_ATTRS = { # Should ATTR_PROFILE be in here?
+ ATTR_COLOR_NAME,
+ ATTR_COLOR_TEMP_KELVIN,
+ ATTR_HS_COLOR,
+ ATTR_RGB_COLOR,
+ ATTR_XY_COLOR,
+}
+
+BRIGHTNESS_ATTRS = {
+ ATTR_BRIGHTNESS,
+ ATTR_BRIGHTNESS_PCT,
+ ATTR_BRIGHTNESS_STEP,
+ ATTR_BRIGHTNESS_STEP_PCT,
+}
+
+ServiceData = dict[str, Any]
+
+
+def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]:
+ """Splits the service data by the adapted attributes, i.e., into separate data
+ items for brightness and color.
+ """
+
+ common_attrs = {ATTR_ENTITY_ID}
+ common_data = {k: service_data[k] for k in common_attrs if k in service_data}
+
+ attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS]
+ service_datas = []
+
+ for attributes in attributes_split_sequence:
+ split_data = {
+ attribute: service_data[attribute]
+ for attribute in attributes
+ if service_data.get(attribute)
+ }
+ if split_data:
+ service_datas.append(common_data | split_data)
+
+ # Distribute the transition duration across all service calls
+ if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None:
+ transition = service_data[ATTR_TRANSITION] / len(service_datas)
+
+ for service_data in service_datas:
+ service_data[ATTR_TRANSITION] = transition
+
+ return service_datas
+
+
+def _filter_service_data(service_data: ServiceData, state: State | None) -> ServiceData:
+ """Filter service data by removing attributes that already equal the given state.
+
+ Removes all attributes from service call data whose values are already present
+ in the target entity's state."""
+
+ if not state:
+ return service_data
+
+ filtered_service_data = {
+ k: service_data[k]
+ for k in service_data.keys()
+ if k not in state.attributes or service_data[k] != state.attributes[k]
+ }
+
+ return filtered_service_data
+
+
+def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool:
+ """Determines whether the service data justifies an adaptation service call.
+
+ A service call is not justified for data which does not contain any entries that
+ change relevant attributes of an adapting entity, e.g., brightness or color."""
+ common_attrs = {ATTR_ENTITY_ID, ATTR_TRANSITION}
+ relevant_attrs = set(service_data) - common_attrs
+
+ return bool(relevant_attrs)
+
+
+async def _create_service_call_data_iterator(
+ hass: HomeAssistant,
+ service_datas: list[ServiceData],
+ filter_by_state: bool = False,
+) -> AsyncGenerator[ServiceData, None]:
+ """Enumerates and filters a list of service datas on the fly.
+
+ If filtering is enabled, every service data is filtered by the current state of
+ the related entity and only returned if it contains relevant data that justifies
+ a service call.
+ The main advantage of this generator over a list is that it applies the filter
+ at the time when the service data is read instead of up front. This gives greater
+ flexibility because entity states can change while the items are iterated.
+ """
+
+ for service_data in service_datas:
+ if filter_by_state and (entity_id := service_data.get(ATTR_ENTITY_ID)):
+ current_entity_state = hass.states.get(entity_id)
+
+ # Filter data to remove attributes that equal the current state
+ if current_entity_state:
+ service_data = _filter_service_data(service_data, current_entity_state)
+
+ # Emit service data if it still contains relevant attributes (else try next)
+ if _has_relevant_service_data_attributes(service_data):
+ yield service_data
+ else:
+ yield service_data
+
+
+@dataclass
+class AdaptationData:
+ """Holds all data required to execute an adaptation."""
+
+ entity_id: str
+ context: Context
+ sleep_time: float
+ service_call_datas: AsyncGenerator[ServiceData, None]
+
+ async def next_service_call_data(self) -> ServiceData | None:
+ """Return data for the next service call, or none if no more data exists."""
+ return await anext(self.service_call_datas, None)
+
+
+def prepare_adaptation_data(
+ hass: HomeAssistant,
+ entity_id: str,
+ context: Context,
+ transition: float | None,
+ split_delay: float,
+ service_data: ServiceData,
+ split: bool,
+ filter_by_state: bool,
+) -> AdaptationData:
+ service_datas = (
+ [service_data] if not split else _split_service_call_data(service_data)
+ )
+
+ sleep_time = (
+ transition / max(1, len(service_datas)) if transition is not None else 0
+ ) + split_delay
+
+ service_data_iterator = _create_service_call_data_iterator(
+ hass, service_datas, filter_by_state
+ )
+
+ return AdaptationData(entity_id, context, sleep_time, service_data_iterator)
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index 6d36ae2e..3aaf9dda 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -176,6 +176,17 @@ DOCS[CONF_AUTORESET_CONTROL] = (
"Set to 0 to disable. ⏲️"
)
+CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = (
+ "skip_redundant_commands",
+ False,
+)
+DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = (
+ "Skip sending adaptation commands whose target state already "
+ "equals the light's known state. Minimizes network traffic and improves the "
+ "adaptation responsivity in some situations. "
+ "Disable if physical light states get out of sync with HA's recorded state."
+)
+
SLEEP_MODE_SWITCH = "sleep_mode_switch"
ADAPT_COLOR_SWITCH = "adapt_color_switch"
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
@@ -271,6 +282,11 @@ VALIDATION_TUPLES = [
DEFAULT_AUTORESET_CONTROL,
int_between(0, 365 * 24 * 60 * 60), # 1 year max
),
+ (
+ CONF_SKIP_REDUNDANT_COMMANDS,
+ DEFAULT_SKIP_REDUNDANT_COMMANDS,
+ bool,
+ ),
]
CONST_COLOR = "color"
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index 54af5e11..00126027 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -47,7 +47,8 @@
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
- "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️"
+ "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "skip_redundant_commands": "Experimental: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state."
}
}
},
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 4d36fce1..a8707bcf 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -17,10 +17,6 @@ from typing import Any, Literal
import astral
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
- ATTR_BRIGHTNESS_PCT,
- ATTR_BRIGHTNESS_STEP,
- ATTR_BRIGHTNESS_STEP_PCT,
- ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ATTR_MAX_COLOR_TEMP_KELVIN,
@@ -96,6 +92,12 @@ import homeassistant.util.dt as dt_util
import ulid_transform
import voluptuous as vol
+from .adaptation_utils import (
+ BRIGHTNESS_ATTRS,
+ COLOR_ATTRS,
+ AdaptationData,
+ prepare_adaptation_data,
+)
from .const import (
ADAPT_BRIGHTNESS_SWITCH,
ADAPT_COLOR_SWITCH,
@@ -121,6 +123,7 @@ from .const import (
CONF_PREFER_RGB_COLOR,
CONF_SEND_SPLIT_DELAY,
CONF_SEPARATE_TURN_ON_COMMANDS,
+ CONF_SKIP_REDUNDANT_COMMANDS,
CONF_SLEEP_BRIGHTNESS,
CONF_SLEEP_COLOR_TEMP,
CONF_SLEEP_RGB_COLOR,
@@ -184,26 +187,10 @@ BRIGHTNESS_CHANGE = 25 # ≈10% of total range
COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500)
RGB_REDMEAN_CHANGE = 80 # ≈10% of total range
-COLOR_ATTRS = { # Should ATTR_PROFILE be in here?
- ATTR_COLOR_NAME,
- ATTR_COLOR_TEMP_KELVIN,
- ATTR_HS_COLOR,
- ATTR_RGB_COLOR,
- ATTR_XY_COLOR,
-}
-
-BRIGHTNESS_ATTRS = {
- ATTR_BRIGHTNESS,
- ATTR_BRIGHTNESS_PCT,
- ATTR_BRIGHTNESS_STEP,
- ATTR_BRIGHTNESS_STEP_PCT,
-}
# Keep a short domain version for the context instances (which can only be 36 chars)
_DOMAIN_SHORT = "al"
-ServiceData = dict[str, Any]
-
def _int_to_base36(num: int) -> str:
"""
@@ -282,42 +269,6 @@ def is_our_context(context: Context | None) -> bool:
return f":{_DOMAIN_SHORT}:" in context.id
-def _prepare_service_calls(service_data: ServiceData, split=False) -> list[ServiceData]:
- """Prepares the service data for service calls.
-
- Processes the service_data according to the config flags, optionally splitting
- it into multiple data items for the separate adaptation of different attributes.
- Returns a list of service_datas that indicates the required service calls. If
- no splitting is necessary, the output is a list with a single item.
- """
- if not split:
- return [service_data]
-
- common_attrs = {ATTR_ENTITY_ID}
- common_data = {k: service_data[k] for k in common_attrs if k in service_data}
-
- attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS]
- service_datas = []
-
- for attributes in attributes_split_sequence:
- split_data = {
- attribute: service_data[attribute]
- for attribute in attributes
- if service_data.get(attribute)
- }
- if split_data:
- service_datas.append(common_data | split_data)
-
- # Distribute the transition duration across all service calls
- if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None:
- transition = service_data[ATTR_TRANSITION] / len(service_datas)
-
- for service_data in service_datas:
- service_data[ATTR_TRANSITION] = transition
-
- return service_datas
-
-
def _get_switches_with_lights(
hass: HomeAssistant, lights: list[str]
) -> list[AdaptiveSwitch]:
@@ -530,7 +481,6 @@ async def async_setup_entry(
data[ATTR_ADAPT_BRIGHTNESS],
data[ATTR_ADAPT_COLOR],
data[CONF_PREFER_RGB_COLOR],
- force=True,
context=switch.create_context(
"service", parent=service_call.context
),
@@ -937,6 +887,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
self._take_over_control = True
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
+ self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS]
self._expand_light_groups() # updates manual control timers
_loc = get_astral_location(self.hass)
if isinstance(_loc, tuple):
@@ -1134,7 +1085,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
adapt_brightness: bool | None = None,
adapt_color: bool | None = None,
prefer_rgb_color: bool | None = None,
- force: bool = False,
context: Context | None = None,
) -> None:
lock = self._locks.get(light)
@@ -1160,7 +1110,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
features, supports_colors = _supported_features(self.hass, light)
# Check transition == 0 to fix #378
- if ATTR_TRANSITION in features and transition > 0:
+ use_transition = ATTR_TRANSITION in features and transition > 0
+ if use_transition:
service_data[ATTR_TRANSITION] = transition
if ATTR_BRIGHTNESS in features and adapt_brightness:
brightness = round(255 * self._settings["brightness_pct"] / 100)
@@ -1188,52 +1139,59 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context = context or self.create_context("adapt_lights")
- # See #80. Doesn't check if transitions differ but it does the job.
- last_service_data = self.turn_on_off_listener.last_service_data
- if not force and last_service_data.get(light) == service_data:
- _LOGGER.debug(
- "%s: Cancelling adapt to light %s, there's no new values to set (context.id='%s')",
- self._name,
- light,
- context.id,
- )
- return
- else:
- self.turn_on_off_listener.last_service_data[light] = service_data
+ self.turn_on_off_listener.last_service_data[light] = service_data
- service_datas = _prepare_service_calls(
- service_data, self._separate_turn_on_commands
+ data = prepare_adaptation_data(
+ self.hass,
+ light,
+ context,
+ transition if use_transition else 0,
+ self._send_split_delay / 1000.0,
+ service_data,
+ split=self._separate_turn_on_commands,
+ filter_by_state=self._skip_redundant_commands,
)
- await self._make_cancellable_adaptation_calls(service_datas, context, light)
- async def _make_adaptation_calls(
- self, service_datas: list[ServiceData], context: Context
- ):
+ await self._execute_cancellable_adaptation_calls(data)
+
+ async def _execute_adaptation_calls(self, data: AdaptationData):
"""Executes a sequence of adaptation service calls for the given service datas."""
- for i, service_data in enumerate(service_datas):
- is_first_call = i == 0
- # Sleep _between_ multiple service calls, but not before the first or a single one.
+ index = 0
+ while True:
+ is_first_call = index == 0
+ index += 1
+
+ # Sleep between multiple service calls.
if not is_first_call:
- await asyncio.sleep(service_data.get(ATTR_TRANSITION, 0))
- await asyncio.sleep(self._send_split_delay / 1000.0)
+ await asyncio.sleep(data.sleep_time)
+
+ # Instead of directly iterating the generator in the while-loop, we get
+ # the next item here after the sleep to make sure it incorporates state
+ # changes which happened during the sleep.
+ service_data = await data.next_service_call_data()
+
+ if not service_data:
+ # All service datas processed
+ break
_LOGGER.debug(
"%s: Scheduling 'light.turn_on' with the following 'service_data': %s"
" with context.id='%s'",
self._name,
service_data,
- context.id,
+ data.context.id,
)
await self.hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
service_data,
- context=context,
+ context=data.context,
)
- async def _make_cancellable_adaptation_calls(
- self, service_datas: list[ServiceData], context: Context, light_id: str
+ async def _execute_cancellable_adaptation_calls(
+ self,
+ data: AdaptationData,
):
"""Executes a cancellable sequence of adaptation service calls for the given service datas.
@@ -1241,18 +1199,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
to cancel an ongoing adaptation when a light is turned off.
"""
# Prevent overlap of multiple adaptation sequences
- self.turn_on_off_listener.cancel_ongoing_adaptation_calls(light_id)
+ self.turn_on_off_listener.cancel_ongoing_adaptation_calls(data.entity_id)
# Execute adaptation calls within a task
try:
- task = self.turn_on_off_listener.adaptation_tasks[
- light_id
- ] = asyncio.ensure_future(
- self._make_adaptation_calls(service_datas, context)
- )
+ task = asyncio.ensure_future(self._execute_adaptation_calls(data))
+ self.turn_on_off_listener.adaptation_tasks[data.entity_id] = task
await task
except asyncio.CancelledError:
- _LOGGER.debug("Ongoing adaptation of %s cancelled", light_id)
+ _LOGGER.debug("Ongoing adaptation of %s cancelled", data.entity_id)
async def _update_attrs_and_maybe_adapt_lights(
self,
@@ -1358,7 +1313,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
else:
_fire_manual_control_event(self, light, context)
else:
- await self._adapt_light(light, transition, force=force, context=context)
+ await self._adapt_light(light, transition, context=context)
async def _sleep_mode_switch_state_event(self, event: Event) -> None:
if not match_switch_state_event(event, (STATE_ON, STATE_OFF)):
diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json
index cc427138..79eb0df8 100644
--- a/custom_components/adaptive_lighting/translations/de.json
+++ b/custom_components/adaptive_lighting/translations/de.json
@@ -45,7 +45,8 @@
"take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.",
"detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)",
"transition": "transition, Wechselzeit in Sekunden",
- "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden."
+ "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.",
+ "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben."
}
}
},
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index 38fb7b0e..a930a8ce 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -48,7 +48,8 @@
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
- "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️"
+ "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "skip_redundant_commands": "Experimental: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state."
}
}
},
diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py
new file mode 100644
index 00000000..b1cfcb5b
--- /dev/null
+++ b/tests/test_adaptation_utils.py
@@ -0,0 +1,336 @@
+"""Tests for Adaptive Lighting utils."""
+
+from unittest.mock import Mock
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS,
+ ATTR_COLOR_TEMP_KELVIN,
+ ATTR_TRANSITION,
+)
+from homeassistant.const import ATTR_ENTITY_ID, STATE_ON
+from homeassistant.core import Context, State
+import pytest
+
+from custom_components.adaptive_lighting.adaptation_utils import (
+ ServiceData,
+ _create_service_call_data_iterator,
+ _filter_service_data,
+ _has_relevant_service_data_attributes,
+ _split_service_call_data,
+ prepare_adaptation_data,
+)
+
+
+@pytest.mark.parametrize(
+ "input_data,expected_data_list",
+ [
+ (
+ {"foo": 1},
+ [],
+ ),
+ (
+ {ATTR_BRIGHTNESS: 10},
+ [{ATTR_BRIGHTNESS: 10}],
+ ),
+ (
+ {ATTR_COLOR_TEMP_KELVIN: 3500},
+ [{ATTR_COLOR_TEMP_KELVIN: 3500}],
+ ),
+ (
+ {ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10},
+ [{ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}],
+ ),
+ (
+ {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500},
+ [{ATTR_BRIGHTNESS: 10}, {ATTR_COLOR_TEMP_KELVIN: 3500}],
+ ),
+ (
+ {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 2},
+ [
+ {ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 1},
+ {ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 1},
+ ],
+ ),
+ (
+ {ATTR_TRANSITION: 1},
+ [],
+ ),
+ ],
+ ids=[
+ "remove irrelevant attributes",
+ "brightness only yields one service call",
+ "color only yields one service call",
+ "include entity ID",
+ "brightness and color are split into two with brightness first",
+ "transition time is distributed among service calls",
+ "ignore transition time without service calls",
+ ],
+)
+async def test_split_service_call_data(input_data, expected_data_list):
+ """Test splitting of service call data."""
+ assert _split_service_call_data(input_data) == expected_data_list
+
+
+@pytest.mark.parametrize(
+ "service_data,state,service_data_expected",
+ [
+ (
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
+ None,
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
+ ),
+ (
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
+ State("light.test", STATE_ON),
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
+ ),
+ (
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
+ State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 10}),
+ {ATTR_ENTITY_ID: "light.test", ATTR_TRANSITION: 2},
+ ),
+ (
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
+ State("light.test", STATE_ON, {ATTR_BRIGHTNESS: 11}),
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
+ ),
+ ],
+ ids=[
+ "pass all attributes on missing state",
+ "pass all attributes on empty state",
+ "remove attributes whose values equal the state",
+ "keep attributes whose values differ from the state",
+ ],
+)
+async def test_filter_service_data(
+ service_data: ServiceData, state: State | None, service_data_expected: ServiceData
+):
+ """Test filtering of service data."""
+ assert _filter_service_data(service_data, state) == service_data_expected
+
+
+@pytest.mark.parametrize(
+ "service_data,expected_relevant",
+ [
+ (
+ {ATTR_ENTITY_ID: "light.test"},
+ False,
+ ),
+ (
+ {ATTR_TRANSITION: 2},
+ False,
+ ),
+ (
+ {ATTR_BRIGHTNESS: 10},
+ True,
+ ),
+ (
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
+ True,
+ ),
+ ],
+)
+async def test_has_relevant_service_data_attributes(
+ service_data: ServiceData, expected_relevant: bool
+):
+ """Test the determination of relevancy of service data"""
+ assert _has_relevant_service_data_attributes(service_data) == expected_relevant
+
+
+@pytest.mark.parametrize(
+ "service_datas,filter_by_state,service_datas_expected",
+ [
+ (
+ [{ATTR_ENTITY_ID: "light.test"}],
+ False,
+ [{ATTR_ENTITY_ID: "light.test"}],
+ ),
+ (
+ [{ATTR_ENTITY_ID: "light.test"}, {ATTR_ENTITY_ID: "light.test2"}],
+ False,
+ [{ATTR_ENTITY_ID: "light.test"}, {ATTR_ENTITY_ID: "light.test2"}],
+ ),
+ (
+ [{ATTR_ENTITY_ID: "light.test"}],
+ True,
+ [],
+ ),
+ (
+ [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10}],
+ True,
+ [],
+ ),
+ (
+ [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}],
+ True,
+ [{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11}],
+ ),
+ (
+ [
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11},
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22},
+ ],
+ True,
+ [
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 11},
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22},
+ ],
+ ),
+ (
+ [
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10},
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22},
+ ],
+ True,
+ [
+ {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 22},
+ ],
+ ),
+ ],
+ ids=[
+ "single item passed through without filtering",
+ "two items passed through without filtering",
+ "filter removes item without relevant attributes",
+ "filter removes item with relevant attribute that equals the state",
+ "filter keeps item with relevant attribute that is different from state",
+ "filter keeps two items with relevant attributes that are different from state",
+ "filter removes item that equals state and keeps items that differs from state",
+ ],
+)
+async def test_create_service_call_data_iterator(
+ service_datas: list[ServiceData],
+ filter_by_state: bool,
+ service_datas_expected: list[ServiceData],
+ hass_states_mock,
+):
+ """Test the generator function for correct enumeration and filtering."""
+
+ generated_service_datas = [
+ data
+ async for data in _create_service_call_data_iterator(
+ hass_states_mock, service_datas, filter_by_state
+ )
+ ]
+
+ assert generated_service_datas == service_datas_expected
+ assert (
+ hass_states_mock.states.get.call_count == 0
+ if not filter_by_state
+ else len(service_datas)
+ )
+
+
+@pytest.mark.parametrize(
+ "service_data,split,filter_by_state,service_datas_expected,sleep_time_expected",
+ [
+ (
+ {
+ ATTR_ENTITY_ID: "light.test",
+ ATTR_BRIGHTNESS: 10,
+ ATTR_COLOR_TEMP_KELVIN: 4000,
+ },
+ False,
+ False,
+ [
+ {
+ ATTR_ENTITY_ID: "light.test",
+ ATTR_BRIGHTNESS: 10,
+ ATTR_COLOR_TEMP_KELVIN: 4000,
+ }
+ ],
+ 1.2,
+ ),
+ (
+ {
+ ATTR_ENTITY_ID: "light.test",
+ ATTR_BRIGHTNESS: 10,
+ ATTR_COLOR_TEMP_KELVIN: 4000,
+ },
+ True,
+ False,
+ [
+ {
+ ATTR_ENTITY_ID: "light.test",
+ ATTR_BRIGHTNESS: 10,
+ },
+ {
+ ATTR_ENTITY_ID: "light.test",
+ ATTR_COLOR_TEMP_KELVIN: 4000,
+ },
+ ],
+ 0.7,
+ ),
+ (
+ {
+ ATTR_ENTITY_ID: "light.test",
+ ATTR_BRIGHTNESS: 10,
+ ATTR_COLOR_TEMP_KELVIN: 4000,
+ },
+ False,
+ True,
+ [
+ {
+ ATTR_ENTITY_ID: "light.test",
+ ATTR_COLOR_TEMP_KELVIN: 4000,
+ }
+ ],
+ 1.2,
+ ),
+ (
+ {
+ ATTR_ENTITY_ID: "light.test",
+ ATTR_BRIGHTNESS: 10,
+ ATTR_COLOR_TEMP_KELVIN: 4000,
+ },
+ True,
+ True,
+ [
+ {
+ ATTR_ENTITY_ID: "light.test",
+ ATTR_COLOR_TEMP_KELVIN: 4000,
+ }
+ ],
+ 0.7,
+ ),
+ ],
+ ids=[
+ "service data passed through",
+ "service data split",
+ "service data filtered",
+ "service data split and filtered",
+ ],
+)
+async def test_prepare_adaptation_data(
+ hass_states_mock,
+ service_data,
+ split,
+ filter_by_state,
+ service_datas_expected,
+ sleep_time_expected,
+):
+ """Test creation of correct service data objects."""
+ data = prepare_adaptation_data(
+ hass_states_mock,
+ "test.entity",
+ Context(id="test-id"),
+ 1,
+ 0.2,
+ service_data,
+ split,
+ filter_by_state,
+ )
+
+ generated_service_datas = [item async for item in data.service_call_datas]
+
+ assert data.entity_id == "test.entity"
+ assert data.context.id == "test-id"
+ assert data.sleep_time == sleep_time_expected
+ assert generated_service_datas == service_datas_expected
+
+
+@pytest.fixture(name="hass_states_mock")
+def fixture_hass_states_mock():
+ """Mocks a HA state machine which returns a mock state."""
+ hass = Mock()
+ hass.states.get.return_value = Mock(attributes={ATTR_BRIGHTNESS: 10})
+ return hass
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 0e46f458..bb5d1c49 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -50,6 +50,10 @@ from pytest_homeassistant_custom_component.common import (
import ulid_transform
import voluptuous.error
+from custom_components.adaptive_lighting.adaptation_utils import (
+ AdaptationData,
+ _create_service_call_data_iterator,
+)
from custom_components.adaptive_lighting.const import (
ADAPT_BRIGHTNESS_SWITCH,
ADAPT_COLOR_SWITCH,
@@ -84,7 +88,6 @@ from custom_components.adaptive_lighting.switch import (
_SUPPORT_OPTS,
VALID_COLOR_MODES,
_attributes_have_changed,
- _prepare_service_calls,
_supported_features,
color_difference_redmean,
create_context,
@@ -1404,69 +1407,6 @@ async def test_change_switch_settings_service(hass):
assert switch._sun_light_settings.min_color_temp == 2500
-@pytest.mark.parametrize(
- "service_data_input,split,service_data_expected",
- [
- (
- {"foo": 1, ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
- False,
- [{"foo": 1, ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2}],
- ),
- (
- {"foo": 1},
- True,
- [],
- ),
- (
- {ATTR_BRIGHTNESS: 10},
- True,
- [{ATTR_BRIGHTNESS: 10}],
- ),
- (
- {ATTR_COLOR_TEMP_KELVIN: 3500},
- True,
- [{ATTR_COLOR_TEMP_KELVIN: 3500}],
- ),
- (
- {ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10},
- True,
- [{ATTR_ENTITY_ID: "foo", ATTR_BRIGHTNESS: 10}],
- ),
- (
- {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500},
- True,
- [{ATTR_BRIGHTNESS: 10}, {ATTR_COLOR_TEMP_KELVIN: 3500}],
- ),
- (
- {ATTR_BRIGHTNESS: 10, ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 2},
- True,
- [
- {ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 1},
- {ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 1},
- ],
- ),
- (
- {ATTR_TRANSITION: 1},
- True,
- [],
- ),
- ],
- ids=[
- "pass through when splitting is disabled",
- "remove irrelevant attributes",
- "brightness only yields one service call",
- "color only yields one service call",
- "include entity ID",
- "brightness and color are split into two with brightness first",
- "transition time is distributed among service calls",
- "ignore transition time without service calls",
- ],
-)
-async def test_prepare_service_calls(service_data_input, split, service_data_expected):
- """Test the preparation of service calls, e.g., splitting."""
- assert _prepare_service_calls(service_data_input, split) == service_data_expected
-
-
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_cancellable_service_calls_task(hass):
"""Test the creation and execution of the task that wraps adaptation service calls."""
@@ -1476,17 +1416,18 @@ async def test_cancellable_service_calls_task(hass):
assert switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) is None
- await switch._make_cancellable_adaptation_calls(
- [
- {
- ATTR_BRIGHTNESS: 10,
- ATTR_COLOR_TEMP_KELVIN: 10,
- ATTR_ENTITY_ID: light.entity_id,
- }
- ],
- context,
+ service_data = {
+ ATTR_BRIGHTNESS: 10,
+ ATTR_COLOR_TEMP_KELVIN: 10,
+ ATTR_ENTITY_ID: light.entity_id,
+ }
+ adaptation_data = AdaptationData(
light.entity_id,
+ context,
+ 0,
+ _create_service_call_data_iterator(hass, [service_data]),
)
+ await switch._execute_cancellable_adaptation_calls(adaptation_data)
task = switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id)
assert task is not None
From d16a9a5751bea85ee5ee897863916816361456e3 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Sun, 2 Jul 2023 15:32:44 -0700
Subject: [PATCH 040/549] [pre-commit.ci] pre-commit autoupdate (#620)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.6.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.6.0...v3.7.0)
* Update README.md, strings.json, and services.yaml
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt
Co-authored-by: github-actions[bot]
---
.pre-commit-config.yaml | 2 +-
README.md | 64 +++++++++----------
.../adaptive_lighting/strings.json | 2 +-
.../adaptive_lighting/translations/en.json | 2 +-
4 files changed, 35 insertions(+), 35 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d8c823c4..8c35e268 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
hooks:
- id: black
- repo: https://github.com/asottile/pyupgrade
- rev: v3.6.0
+ rev: v3.7.0
hooks:
- id: pyupgrade
args: ["--py39-plus"]
diff --git a/README.md b/README.md
index e0ba67fc..20a21dad 100644
--- a/README.md
+++ b/README.md
@@ -91,38 +91,38 @@ The YAML and frontend configuration methods support all of the options listed be
-| Variable name | Description | Default | Type |
-| :------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------- | :----------------------------------- |
-| `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` |
-| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
-| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
-| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
-| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
-| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
-| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
-| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
-| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
-| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
-| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
-| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
-| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
-| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
-| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
-| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
-| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
-| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
-| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
-| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
-| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
-| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
-| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
-| `send_split_delay` | 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 |
-| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
+| Variable name | Description | Default | Type |
+|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
+| `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` |
+| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
+| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
+| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
+| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
+| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
+| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
+| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
+| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
+| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
+| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
+| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
+| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
+| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
+| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
+| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
+| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
+| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
+| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
+| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
+| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
+| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
+| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
+| `send_split_delay` | 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 |
+| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index 00126027..7ad84741 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -48,7 +48,7 @@
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
- "skip_redundant_commands": "Experimental: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state."
+ "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state."
}
}
},
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index a930a8ce..e6d1f9a9 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -49,7 +49,7 @@
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
- "skip_redundant_commands": "Experimental: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state."
+ "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state."
}
}
},
From ed80bd78298e5860d382485275c99f762eeb3f14 Mon Sep 17 00:00:00 2001
From: Mario Guggenberger
Date: Wed, 19 Jul 2023 09:10:21 +0200
Subject: [PATCH 041/549] feat: service call adaptation (#628)
* feat: service call adaptation
* feat: toggle-on service call adaptation
* feat: prefer service call transition
---
.../adaptive_lighting/__init__.py | 3 +-
.../adaptive_lighting/adaptation_utils.py | 2 +
.../adaptive_lighting/hass_utils.py | 64 ++++
custom_components/adaptive_lighting/switch.py | 315 +++++++++++++++---
tests/test_hass_utils.py | 81 +++++
tests/test_switch.py | 212 +++++++++++-
6 files changed, 610 insertions(+), 67 deletions(-)
create mode 100644 custom_components/adaptive_lighting/hass_utils.py
create mode 100644 tests/test_hass_utils.py
diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py
index 07a0a758..f985e8c5 100755
--- a/custom_components/adaptive_lighting/__init__.py
+++ b/custom_components/adaptive_lighting/__init__.py
@@ -89,8 +89,7 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool:
if len(data) == 1 and ATTR_TURN_ON_OFF_LISTENER in data:
# no more config_entries
turn_on_off_listener = data.pop(ATTR_TURN_ON_OFF_LISTENER)
- turn_on_off_listener.remove_listener()
- turn_on_off_listener.remove_listener2()
+ turn_on_off_listener.disable()
if not data:
hass.data.pop(DOMAIN)
diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py
index 0cfbc7dd..599805a6 100644
--- a/custom_components/adaptive_lighting/adaptation_utils.py
+++ b/custom_components/adaptive_lighting/adaptation_utils.py
@@ -133,6 +133,7 @@ class AdaptationData:
context: Context
sleep_time: float
service_call_datas: AsyncGenerator[ServiceData, None]
+ initial_sleep: bool = False
async def next_service_call_data(self) -> ServiceData | None:
"""Return data for the next service call, or none if no more data exists."""
@@ -149,6 +150,7 @@ def prepare_adaptation_data(
split: bool,
filter_by_state: bool,
) -> AdaptationData:
+ "Prepares a data object carrying all data required to execute an adaptation."
service_datas = (
[service_data] if not split else _split_service_call_data(service_data)
)
diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py
new file mode 100644
index 00000000..5a195bcc
--- /dev/null
+++ b/custom_components/adaptive_lighting/hass_utils.py
@@ -0,0 +1,64 @@
+"""Utility functions for HA core."""
+from collections.abc import Awaitable
+from typing import Callable
+
+from homeassistant.core import HomeAssistant, ServiceCall
+from homeassistant.util.read_only_dict import ReadOnlyDict
+
+from .adaptation_utils import ServiceData
+
+
+def setup_service_call_interceptor(
+ hass: HomeAssistant,
+ domain: str,
+ service: str,
+ intercept_func: Callable[[ServiceCall, ServiceData], Awaitable[None] | None],
+) -> Callable[[], None]:
+ """Inject a function into a registered service call to preprocess service data.
+
+ The injected interceptor function receives the service call and a writeable data dictionary
+ (the data of the service call is read-only) before the service call is executed."""
+ try:
+ # HACK: Access protected attribute of HA service registry.
+ # This is necessary to replace a registered service handler with our
+ # proxy handler to intercept calls.
+ registered_services = (
+ hass.services._services # pylint: disable=protected-access
+ )
+ except AttributeError as error:
+ raise RuntimeError(
+ "Intercept failed because registered services are no longer accessible "
+ "(internal API may have changed)"
+ ) from error
+
+ if domain not in registered_services or service not in registered_services[domain]:
+ raise RuntimeError(
+ f"Intercept failed because service {domain}.{service} is not registered"
+ )
+
+ existing_service = registered_services[domain][service]
+
+ async def service_func_proxy(call: ServiceCall) -> None:
+ # Convert read-only data to writeable dictionary for modification by interceptor
+ data = dict(call.data)
+
+ # Call interceptor
+ await intercept_func(call, data)
+
+ # Convert data back to read-only
+ call.data = ReadOnlyDict(data)
+
+ # Call original service handler with processed data
+ await existing_service.job.target(call)
+
+ hass.services.async_register(
+ domain, service, service_func_proxy, existing_service.schema
+ )
+
+ def remove():
+ # Remove the interceptor by reinstalling the original service handler
+ hass.services.async_register(
+ domain, service, existing_service.job.target, existing_service.schema
+ )
+
+ return remove
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index a8707bcf..1789439d 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -41,6 +41,7 @@ from homeassistant.components.light import (
SUPPORT_COLOR_TEMP,
SUPPORT_TRANSITION,
is_on,
+ preprocess_turn_on_alternatives,
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
@@ -54,9 +55,11 @@ from homeassistant.const import (
ATTR_SERVICE_DATA,
ATTR_SUPPORTED_FEATURES,
CONF_NAME,
+ CONF_PARAMS,
EVENT_CALL_SERVICE,
EVENT_HOMEASSISTANT_STARTED,
EVENT_STATE_CHANGED,
+ SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
@@ -96,6 +99,7 @@ from .adaptation_utils import (
BRIGHTNESS_ATTRS,
COLOR_ATTRS,
AdaptationData,
+ ServiceData,
prepare_adaptation_data,
)
from .const import (
@@ -156,6 +160,7 @@ from .const import (
apply_service_schema,
replace_none_str,
)
+from .hass_utils import setup_service_call_interceptor
_SUPPORT_OPTS = {
COLOR_MODE_BRIGHTNESS: SUPPORT_BRIGHTNESS,
@@ -182,6 +187,11 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
+# A (non-user-configurable, thus internal) flag to control the proactive adaptation mode.
+# This exists to disable the proactive adaptation in the unit tests and enable it
+# only for specific unit tests and when running as integration."""
+INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION = "proactive_adaptation"
+
# Consider it a significant change when attribute changes more than
BRIGHTNESS_CHANGE = 25 # ≈10% of total range
COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500)
@@ -262,11 +272,17 @@ def create_context(
return Context(id=context_id, parent_id=parent_id)
+def is_our_context_id(context_id: str | None) -> bool:
+ if context_id is None:
+ return False
+ return f":{_DOMAIN_SHORT}:" in context_id
+
+
def is_our_context(context: Context | None) -> bool:
"""Check whether this integration created 'context'."""
if context is None:
return False
- return f":{_DOMAIN_SHORT}:" in context.id
+ return is_our_context_id(context.id)
def _get_switches_with_lights(
@@ -284,7 +300,7 @@ def _get_switches_with_lights(
all_check_lights = _expand_light_groups(hass, lights)
switch._expand_light_groups()
# Check if any of the lights are in the switch's lights
- if set(switch._lights) & set(all_check_lights):
+ if set(switch.lights) & set(all_check_lights):
switches.append(switch)
return switches
@@ -385,12 +401,12 @@ async def handle_change_switch_settings(
data,
)
- all_lights = switch._lights # pylint: disable=protected-access
+ all_lights = switch.lights # pylint: disable=protected-access
switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False)
if switch.is_on:
await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access
all_lights,
- transition=switch._initial_transition,
+ transition=switch.initial_transition,
force=True,
context=switch.create_context("service", parent=service_call.context),
)
@@ -424,8 +440,8 @@ async def async_setup_entry(
assert config_entry.entry_id in data
if ATTR_TURN_ON_OFF_LISTENER not in data:
- data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass)
- turn_on_off_listener = data[ATTR_TURN_ON_OFF_LISTENER]
+ data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass, config_entry)
+ turn_on_off_listener: TurnOnOffListener = data[ATTR_TURN_ON_OFF_LISTENER]
sleep_mode_switch = SimpleSwitch(
"Sleep Mode", False, hass, config_entry, ICON_SLEEP
)
@@ -443,6 +459,7 @@ async def async_setup_entry(
adapt_color_switch,
adapt_brightness_switch,
)
+ turn_on_off_listener.adaptive_switch = switch
# save our switch instance, allows us to make switch's entity_id optional in service calls.
hass.data[DOMAIN][config_entry.entry_id]["instance"] = switch
@@ -469,7 +486,7 @@ async def async_setup_entry(
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
- all_lights = switch._lights # pylint: disable=protected-access
+ all_lights = switch.lights
else:
all_lights = _expand_light_groups(switch.hass, lights)
switch.turn_on_off_listener.lights.update(all_lights)
@@ -498,7 +515,7 @@ async def async_setup_entry(
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
- all_lights = switch._lights # pylint: disable=protected-access
+ all_lights = switch.lights
else:
all_lights = _expand_light_groups(switch.hass, lights)
if service_call.data[CONF_MANUAL_CONTROL]:
@@ -510,7 +527,7 @@ async def async_setup_entry(
# pylint: disable=protected-access
await switch._update_attrs_and_maybe_adapt_lights(
all_lights,
- transition=switch._initial_transition,
+ transition=switch.initial_transition,
force=True,
context=switch.create_context(
"service", parent=service_call.context
@@ -523,7 +540,7 @@ async def async_setup_entry(
service=SERVICE_APPLY,
service_func=handle_apply,
schema=apply_service_schema(
- switch._initial_transition
+ switch.initial_transition
), # pylint: disable=protected-access
)
@@ -803,7 +820,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._name = data[CONF_NAME]
self._interval = data[CONF_INTERVAL]
- self._lights = data[CONF_LIGHTS]
+ self.lights: list[str] = data[CONF_LIGHTS]
# backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS
self._config_backup = deepcopy(data)
@@ -835,7 +852,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
" config_entry.data: '%s',"
" config_entry.options: '%s', converted to '%s'.",
self._name,
- self._lights,
+ self.lights,
config_entry.data,
config_entry.options,
data,
@@ -868,7 +885,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
attrdata[k] = v.total_seconds()
self._config.update(attrdata)
- self._initial_transition = data[CONF_INITIAL_TRANSITION]
+ self.initial_transition = data[CONF_INITIAL_TRANSITION]
self._sleep_transition = data[CONF_SLEEP_TRANSITION]
self._only_once = data[CONF_ONLY_ONCE]
self._prefer_rgb_color = data[CONF_PREFER_RGB_COLOR]
@@ -921,7 +938,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
_LOGGER.debug(
"%s: Set switch settings for lights '%s'. now using data: '%s'",
self._name,
- self._lights,
+ self.lights,
data,
)
@@ -961,12 +978,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._remove_listeners()
def _expand_light_groups(self) -> None:
- all_lights = _expand_light_groups(self.hass, self._lights)
+ all_lights = _expand_light_groups(self.hass, self.lights)
self.turn_on_off_listener.lights.update(all_lights)
self.turn_on_off_listener.set_auto_reset_manual_control_times(
all_lights, self._auto_reset_manual_control_time
)
- self._lights = list(all_lights)
+ self.lights = list(all_lights)
async def _setup_listeners(self, _=None) -> None:
_LOGGER.debug("%s: Called '_setup_listeners'", self._name)
@@ -987,10 +1004,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self.remove_listeners.extend([remove_interval, remove_sleep])
- if self._lights:
+ if self.lights:
self._expand_light_groups()
remove_state = async_track_state_change_event(
- self.hass, self._lights, self._light_event
+ self.hass, self.lights, self._light_event
)
self.remove_listeners.append(remove_state)
@@ -1014,14 +1031,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
return extra_state_attributes
extra_state_attributes["manual_control"] = [
light
- for light in self._lights
+ for light in self.lights
if self.turn_on_off_listener.manual_control.get(light)
]
extra_state_attributes.update(self._settings)
timers = self.turn_on_off_listener.auto_reset_manual_control_timers
extra_state_attributes["autoreset_time_remaining"] = {
light: time
- for light in self._lights
+ for light in self.lights
if (timer := timers.get(light)) and (time := timer.remaining_time()) > 0
}
return extra_state_attributes
@@ -1054,11 +1071,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if self.is_on:
return
self._state = True
- self.turn_on_off_listener.reset(*self._lights)
+ self.turn_on_off_listener.reset(*self.lights)
await self._setup_listeners()
if adapt_lights:
await self._update_attrs_and_maybe_adapt_lights(
- transition=self._initial_transition,
+ transition=self.initial_transition,
force=True,
context=self.create_context("turn_on"),
)
@@ -1069,7 +1086,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
return
self._state = False
self._remove_listeners()
- self.turn_on_off_listener.reset(*self._lights)
+ self.turn_on_off_listener.reset(*self.lights)
async def _async_update_at_interval(self, now=None) -> None:
await self._update_attrs_and_maybe_adapt_lights(
@@ -1078,7 +1095,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context=self.create_context("interval"),
)
- async def _adapt_light( # noqa: C901
+ async def prepare_adaptation_data(
self,
light: str,
transition: int | None = None,
@@ -1086,11 +1103,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
adapt_color: bool | None = None,
prefer_rgb_color: bool | None = None,
context: Context | None = None,
- ) -> None:
- lock = self._locks.get(light)
- if lock is not None and lock.locked():
- _LOGGER.debug("%s: '%s' is locked", self._name, light)
- return
+ ) -> AdaptationData:
if transition is None:
transition = self._transition
if adapt_brightness is None:
@@ -1141,7 +1154,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self.turn_on_off_listener.last_service_data[light] = service_data
- data = prepare_adaptation_data(
+ return prepare_adaptation_data(
self.hass,
light,
context,
@@ -1152,7 +1165,35 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
filter_by_state=self._skip_redundant_commands,
)
- await self._execute_cancellable_adaptation_calls(data)
+ async def _adapt_light( # noqa: C901
+ self,
+ light: str,
+ transition: int | None = None,
+ adapt_brightness: bool | None = None,
+ adapt_color: bool | None = None,
+ prefer_rgb_color: bool | None = None,
+ context: Context | None = None,
+ ) -> None:
+ lock = self._locks.get(light)
+ if lock is not None and lock.locked():
+ _LOGGER.debug("%s: '%s' is locked", self._name, light)
+ return
+
+ if self.turn_on_off_listener.is_proactively_adapting(context.parent_id):
+ # Skip if adaptation was already executed by the service call interceptor
+ _LOGGER.debug("Skipping reactive adaptation of %s", context.parent_id)
+ return
+
+ data = await self.prepare_adaptation_data(
+ light,
+ transition,
+ adapt_brightness,
+ adapt_color,
+ prefer_rgb_color,
+ context,
+ )
+
+ await self.execute_cancellable_adaptation_calls(data)
async def _execute_adaptation_calls(self, data: AdaptationData):
"""Executes a sequence of adaptation service calls for the given service datas."""
@@ -1163,7 +1204,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
index += 1
# Sleep between multiple service calls.
- if not is_first_call:
+ if not is_first_call or data.initial_sleep:
await asyncio.sleep(data.sleep_time)
# Instead of directly iterating the generator in the while-loop, we get
@@ -1189,7 +1230,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context=data.context,
)
- async def _execute_cancellable_adaptation_calls(
+ async def execute_cancellable_adaptation_calls(
self,
data: AdaptationData,
):
@@ -1231,7 +1272,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self.async_write_ha_state()
if lights is None:
- lights = self._lights
+ lights = self.lights
filtered_lights = []
if not force:
@@ -1323,7 +1364,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
"%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event
)
# Reset the manually controlled status when the "sleep mode" changes
- self.turn_on_off_listener.reset(*self._lights)
+ self.turn_on_off_listener.reset(*self.lights)
await self._update_attrs_and_maybe_adapt_lights(
transition=self._sleep_transition,
force=True,
@@ -1346,7 +1387,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
entity_id,
event.context.id,
)
- self.turn_on_off_listener.reset(entity_id, reset_manual_control=False)
+
+ if (
+ event.context.parent_id
+ and not self.turn_on_off_listener.is_proactively_adapting(
+ event.context.id
+ )
+ ):
+ self.turn_on_off_listener.reset(entity_id, reset_manual_control=False)
+
# Tracks 'off' → 'on' state changes
self._off_to_on_event[entity_id] = event
lock = self._locks.get(entity_id)
@@ -1381,7 +1430,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
await self._update_attrs_and_maybe_adapt_lights(
lights=[entity_id],
- transition=self._initial_transition,
+ transition=self.initial_transition,
force=True,
context=self.create_context("light_event", parent=event.context),
)
@@ -1662,9 +1711,10 @@ class SunLightSettings:
class TurnOnOffListener:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
- def __init__(self, hass: HomeAssistant):
+ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
"""Initialize the TurnOnOffListener that is shared among all switches."""
self.hass = hass
+ data = validate(config_entry)
self.lights = set()
# Tracks 'light.turn_off' service calls
@@ -1689,11 +1739,169 @@ class TurnOnOffListener:
# Track light transitions
self.transition_timers: dict[str, _AsyncSingleShotTimer] = {}
- self.remove_listener = self.hass.bus.async_listen(
- EVENT_CALL_SERVICE, self.turn_on_off_event_listener
+ self.listener_removers = []
+
+ self.listener_removers.append(
+ self.hass.bus.async_listen(
+ EVENT_CALL_SERVICE, self.turn_on_off_event_listener
+ )
)
- self.remove_listener2 = self.hass.bus.async_listen(
- EVENT_STATE_CHANGED, self.state_changed_event_listener
+ self.listener_removers.append(
+ self.hass.bus.async_listen(
+ EVENT_STATE_CHANGED, self.state_changed_event_listener
+ )
+ )
+
+ self.adaptive_switch: AdaptiveSwitch | None
+ self._proactively_adapting_contexts: dict[str, str] = {}
+
+ is_proactive_adaptation_enabled = (
+ data.get(INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, True) is not False
+ )
+
+ if is_proactive_adaptation_enabled:
+ try:
+ self.listener_removers.append(
+ setup_service_call_interceptor(
+ hass,
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ self._service_interceptor_turn_on_handler,
+ )
+ )
+
+ self.listener_removers.append(
+ setup_service_call_interceptor(
+ hass,
+ LIGHT_DOMAIN,
+ SERVICE_TOGGLE,
+ self._service_interceptor_turn_on_handler,
+ )
+ )
+
+ _LOGGER.debug("Proactive adaptation enabled")
+ except RuntimeError:
+ _LOGGER.warning(
+ "Failed to set up service call interceptors, "
+ "falling back to event-reactive mode",
+ exc_info=True,
+ )
+
+ def disable(self):
+ """Disable the listener by removing all subscribed handlers."""
+ for remove in self.listener_removers:
+ remove()
+
+ def set_proactively_adapting(self, context_id: str, entity_id: str) -> None:
+ """Declare the adaptation with the given context ID as proactively adapting,
+ and associate it to an entity ID."""
+ self._proactively_adapting_contexts[context_id] = entity_id
+
+ def is_proactively_adapting(self, context_id: str) -> bool:
+ """Determine whether an adaptation with the given context ID is proactive."""
+ is_proactively_adapting_context = (
+ context_id in self._proactively_adapting_contexts
+ )
+
+ _LOGGER.debug(
+ "is_proactively_adapting_context %s %s",
+ context_id,
+ is_proactively_adapting_context,
+ )
+
+ return is_proactively_adapting_context
+
+ def clear_proactively_adapting(self, entity_id: str) -> None:
+ """Clear all context IDs associated with the given entity ID.
+
+ Call this method to clear past context IDs and avoid a memory leak."""
+ keys = [
+ k for k, v in self._proactively_adapting_contexts.items() if v == entity_id
+ ]
+
+ for key in keys:
+ self._proactively_adapting_contexts.pop(key)
+
+ async def _service_interceptor_turn_on_handler(
+ self, call: ServiceCall, data: ServiceData
+ ):
+ # Don't adapt our own service calls
+ if is_our_context(call.context):
+ return
+
+ entity_ids = self._get_entity_list(data)
+
+ # For simplicity, only service calls affecting a single entity are currently handled.
+ #
+ # To add support for adapting multiple entities, the following properties
+ # need to hold for _all_ entities:
+ # - managed by this AL instance
+ # - not manually controlled
+ # - supporting the same relevant feature set
+ # - off state
+ if len(entity_ids) != 1:
+ return
+
+ entity_id = entity_ids[0]
+
+ if entity_id not in self.adaptive_switch.lights:
+ return
+
+ if self.manual_control.get(entity_id, False):
+ return
+
+ # Prevent adaptation of TURN_ON calls when light is already on,
+ # and of TOGGLE calls when toggling off.
+ if self.hass.states.is_state(entity_id, STATE_ON):
+ return
+
+ _LOGGER.debug(
+ "Intercepted TURN_ON call with data %s (%s)", data, call.context.id
+ )
+
+ self.reset(entity_id, reset_manual_control=False)
+ self.clear_proactively_adapting(entity_id)
+
+ adapt_brightness = self.adaptive_switch.adapt_brightness_switch.is_on or False
+ adapt_color = self.adaptive_switch.adapt_color_switch.is_on or False
+ transition = (
+ data[CONF_PARAMS].get(ATTR_TRANSITION, None)
+ or self.adaptive_switch.initial_transition
+ )
+
+ adaptation_data = await self.adaptive_switch.prepare_adaptation_data(
+ entity_id,
+ transition,
+ adapt_brightness,
+ adapt_color,
+ )
+
+ # Take first adaptation item to apply it to this service call
+ first_service_data = await adaptation_data.next_service_call_data()
+
+ if not first_service_data:
+ return
+
+ # Update/adapt service call data
+ first_service_data.pop(ATTR_ENTITY_ID, None)
+ # This is called as a preprocessing step by the schema validation of the original
+ # service call and needs to be repeated here to also process the added adaptation data.
+ # (A more generic alternative would be re-executing the validation, but that is more
+ # complicated and unstable because it requires transformation of the data object back
+ # into its original service call structure which cannot be reliably done due to the
+ # lack of a bijective mapping.)
+ preprocess_turn_on_alternatives(self.hass, first_service_data)
+ data[CONF_PARAMS].update(first_service_data)
+
+ # Schedule additional service calls for the remaining adaptation data.
+ # We cannot know here whether there is another call to follow (since the
+ # state can change until the next call), so we just schedule it and let
+ # it sort out by itself.
+ self.set_proactively_adapting(call.context.id, entity_id)
+ self.set_proactively_adapting(adaptation_data.context.id, entity_id)
+ adaptation_data.initial_sleep = True
+ asyncio.create_task( # Don't await to avoid blocking the service call
+ self.adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data)
)
def _handle_timer(
@@ -1772,7 +1980,7 @@ class TurnOnOffListener:
continue
await switch._update_attrs_and_maybe_adapt_lights(
[light],
- transition=switch._initial_transition,
+ transition=switch.initial_transition,
force=True,
context=switch.create_context("autoreset"),
)
@@ -1803,19 +2011,13 @@ class TurnOnOffListener:
self.last_service_data.pop(light, None)
self.cancel_ongoing_adaptation_calls(light)
- async def turn_on_off_event_listener(self, event: Event) -> None:
- """Track 'light.turn_off' and 'light.turn_on' service calls."""
- domain = event.data.get(ATTR_DOMAIN)
- if domain != LIGHT_DOMAIN:
- return
+ def _get_entity_list(self, service_data: ServiceData) -> list[str]:
+ entity_ids = []
- service = event.data[ATTR_SERVICE]
- service_data = event.data[ATTR_SERVICE_DATA]
if ATTR_ENTITY_ID in service_data:
entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID])
elif ATTR_AREA_ID in service_data:
area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID])
- entity_ids = []
for area_id in area_ids:
area_entity_ids = area_entities(self.hass, area_id)
for entity_id in area_entity_ids:
@@ -1828,8 +2030,19 @@ class TurnOnOffListener:
_LOGGER.debug(
"No entity_ids or area_ids found in service_data: %s", service_data
)
+
+ return entity_ids
+
+ async def turn_on_off_event_listener(self, event: Event) -> None:
+ """Track 'light.turn_off' and 'light.turn_on' service calls."""
+ domain = event.data.get(ATTR_DOMAIN)
+ if domain != LIGHT_DOMAIN:
return
+ service = event.data[ATTR_SERVICE]
+ service_data = event.data[ATTR_SERVICE_DATA]
+ entity_ids = self._get_entity_list(service_data)
+
if not any(eid in self.lights for eid in entity_ids):
return
diff --git a/tests/test_hass_utils.py b/tests/test_hass_utils.py
new file mode 100644
index 00000000..9f044d8a
--- /dev/null
+++ b/tests/test_hass_utils.py
@@ -0,0 +1,81 @@
+"""Tests for Adaptive Lighting HASS utils."""
+
+from unittest.mock import AsyncMock
+
+from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
+from homeassistant.const import SERVICE_TURN_ON
+from homeassistant.core import ServiceCall
+from homeassistant.util.read_only_dict import ReadOnlyDict
+
+from custom_components.adaptive_lighting.adaptation_utils import ServiceData
+from custom_components.adaptive_lighting.hass_utils import (
+ setup_service_call_interceptor,
+)
+
+
+async def test_setup_service_call_interceptor(hass):
+ """Test setup and removal of service call interceptor."""
+ service_func_mock = AsyncMock()
+ hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock)
+
+ async def service_call():
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {},
+ blocking=True,
+ )
+
+ # Test if service is called
+
+ await service_call()
+ assert service_func_mock.call_count == 1
+
+ # Test if interceptor is called after setup
+
+ intercept_func_mock = AsyncMock()
+ remove_interceptor = setup_service_call_interceptor(
+ hass,
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ intercept_func_mock,
+ )
+
+ await service_call()
+ assert service_func_mock.call_count == 2
+ assert intercept_func_mock.call_count == 1
+
+ # Test if interceptor is no longer called after removal
+
+ remove_interceptor()
+ await service_call()
+ assert service_func_mock.call_count == 3
+ assert intercept_func_mock.call_count == 1
+
+
+async def test_service_call_interceptor_data_manipulation(hass):
+ """Test service call data manipulation by service call interceptor."""
+ service_func_mock = AsyncMock()
+ hass.services.async_register(LIGHT_DOMAIN, SERVICE_TURN_ON, service_func_mock)
+
+ async def intercept_func(call: ServiceCall, data: ServiceData):
+ data["test1"] = "changed"
+ data["test2"] = "added"
+
+ setup_service_call_interceptor(
+ hass,
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ intercept_func,
+ )
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {"test1": "initial"},
+ blocking=True,
+ )
+
+ (service_call,) = service_func_mock.call_args[0]
+ assert service_call.data == {"test1": "changed", "test2": "added"}
+ assert isinstance(service_call.data, ReadOnlyDict)
diff --git a/tests/test_switch.py b/tests/test_switch.py
index bb5d1c49..4ac92c64 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -6,7 +6,8 @@ import datetime
import itertools
import logging
from random import randint
-from unittest.mock import MagicMock, patch
+from typing import Any
+from unittest.mock import MagicMock, Mock, patch
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -31,12 +32,14 @@ from homeassistant.const import (
ATTR_SUPPORTED_FEATURES,
CONF_LIGHTS,
CONF_NAME,
+ EVENT_CALL_SERVICE,
EVENT_STATE_CHANGED,
+ SERVICE_TOGGLE,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
)
-from homeassistant.core import Context, HomeAssistant, State
+from homeassistant.core import Context, Event, HomeAssistant, State
from homeassistant.helpers import entity_registry
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.setup import async_setup_component
@@ -86,12 +89,15 @@ from custom_components.adaptive_lighting.const import (
)
from custom_components.adaptive_lighting.switch import (
_SUPPORT_OPTS,
+ INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION,
VALID_COLOR_MODES,
+ AdaptiveSwitch,
_attributes_have_changed,
_supported_features,
color_difference_redmean,
create_context,
is_our_context,
+ is_our_context_id,
)
_LOGGER = logging.getLogger(__name__)
@@ -118,6 +124,7 @@ LAT_LONG_TZS = [
]
ENTITY_LIGHT = "light.bed_light"
+ENTITY_LIGHT3 = "light.kitchen_lights"
_SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}"
ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}"
ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}"
@@ -143,9 +150,16 @@ def reset_time_zone():
dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE
-async def setup_switch(hass, extra_data):
+async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitch]:
"""Create the switch entry."""
- entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME, **extra_data})
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={
+ CONF_NAME: DEFAULT_NAME,
+ INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: False,
+ **extra_data,
+ },
+ )
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
@@ -190,6 +204,7 @@ async def setup_lights(hass: HomeAssistant):
"set_level": None,
"set_temperature": None,
"set_color": None,
+ "supports_transition_template": True,
},
},
},
@@ -212,7 +227,7 @@ async def setup_lights(hass: HomeAssistant):
return lights
-async def setup_lights_and_switch(hass, extra_conf=None):
+async def setup_lights_and_switch(hass, extra_conf=None, all_lights: bool = False):
"""Create switch and demo lights."""
# Setup demo lights and turn on
lights_instances = await setup_lights(hass)
@@ -228,6 +243,10 @@ async def setup_lights_and_switch(hass, extra_conf=None):
ENTITY_LIGHT,
"light.ceiling_lights",
]
+
+ if all_lights:
+ lights.append(ENTITY_LIGHT3)
+
assert all(hass.states.get(light) is not None for light in lights)
_, switch = await setup_switch(
hass,
@@ -421,7 +440,7 @@ async def test_adaptive_lighting_time_zones_and_sun_settings(
async def test_light_settings(hass):
"""Test that light settings are correctly applied."""
switch, _ = await setup_lights_and_switch(hass)
- lights = switch._lights
+ lights = switch.lights
# Turn on "sleep mode"
await hass.services.async_call(
@@ -525,7 +544,7 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass):
"""Test that lights that are not in a Adaptive Lighting switch aren't tracked."""
switch, _ = await setup_lights_and_switch(hass)
light = "light.kitchen_lights"
- assert light not in switch._lights
+ assert light not in switch.lights
for state in [True, False]:
await hass.services.async_call(
LIGHT_DOMAIN,
@@ -757,11 +776,11 @@ async def test_manual_control(hass):
await switch.adapt_brightness_switch.async_turn_on()
# Check that when no lights are specified, all are reset
- await change_manual_control(True, {CONF_LIGHTS: switch._lights})
- assert all([manual_control[eid] for eid in switch._lights])
+ await change_manual_control(True, {CONF_LIGHTS: switch.lights})
+ assert all([manual_control[eid] for eid in switch.lights])
# do not pass "lights" so reset all
await change_manual_control(False, {})
- assert all([not manual_control[eid] for eid in switch._lights])
+ assert all([not manual_control[eid] for eid in switch.lights])
@pytest.mark.dependency(depends=[*GLOBAL_TEST_DEPENDENCIES, "test_manual_control"])
@@ -820,7 +839,7 @@ async def test_apply_service(hass):
"""Test adaptive_lighting.apply service."""
switch, (_, _, light) = await setup_lights_and_switch(hass)
entity_id = light.entity_id
- assert entity_id not in switch._lights
+ assert entity_id not in switch.lights
def increased_brightness():
return (light._attr_brightness + 100) % 255
@@ -1074,7 +1093,7 @@ async def test_state_change_handlers(hass):
current_service_data = switch.turn_on_off_listener.last_service_data
assert current_service_data != last_service_data
- for light in switch._lights:
+ for light in switch.lights:
# current_service_data should have changed after the last update.
assert current_service_data.get(light)
assert last_service_data.get(light)
@@ -1359,7 +1378,7 @@ async def test_change_switch_settings_service(hass):
"""Test adaptive_lighting.change_switch_settings service."""
switch, (_, _, light) = await setup_lights_and_switch(hass)
entity_id = light.entity_id
- assert entity_id not in switch._lights
+ assert entity_id not in switch.lights
async def change_switch_settings(**kwargs):
await hass.services.async_call(
@@ -1427,7 +1446,7 @@ async def test_cancellable_service_calls_task(hass):
0,
_create_service_call_data_iterator(hass, [service_data]),
)
- await switch._execute_cancellable_adaptation_calls(adaptation_data)
+ await switch.execute_cancellable_adaptation_calls(adaptation_data)
task = switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id)
assert task is not None
@@ -1451,3 +1470,168 @@ async def test_service_calls_task_cancellation(hass):
pass
assert task.cancelled()
+
+
+async def _turn_on_and_track_event_contexts(
+ hass: HomeAssistant, context_id: str, entity_id
+):
+ context = Context(id=context_id)
+ event_context_ids = []
+
+ async def turn_on_off_event_listener(event: Event) -> None:
+ event_context_ids.append(event.context.id)
+
+ hass.bus.async_listen(EVENT_CALL_SERVICE, turn_on_off_event_listener)
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: entity_id},
+ blocking=True,
+ context=context,
+ )
+ await hass.async_block_till_done()
+
+ return event_context_ids
+
+
+def _mock_sun_light_settings(switch: AdaptiveSwitch, settings: dict[str, Any]):
+ sun_light_settings_mock = Mock()
+ sun_light_settings_mock.get_settings = Mock(return_value=settings)
+ switch._sun_light_settings = sun_light_settings_mock
+
+
+async def test_proactive_adaptation(hass):
+ """Validate that a proactive adaptation updates the original service call."""
+ switch, _ = await setup_lights_and_switch(
+ hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True
+ )
+
+ _mock_sun_light_settings(
+ switch,
+ {
+ ATTR_BRIGHTNESS_PCT: 67,
+ ATTR_COLOR_TEMP_KELVIN: 3448,
+ },
+ )
+
+ event_context_ids = await _turn_on_and_track_event_contexts(
+ hass, "test_context", ENTITY_LIGHT3
+ )
+
+ # Expect a single service call
+ assert len(event_context_ids) == 1
+ assert event_context_ids == ["test_context"]
+
+ # Expect adapted light state
+ state = hass.states.get(ENTITY_LIGHT3)
+ # Sun light settings use %, state only contains absolute
+ assert state.attributes[ATTR_BRIGHTNESS] == 171 # == 67%
+ assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448
+
+
+async def test_proactive_adaptation_with_separate_commands(hass):
+ """Validate that a split proactive adaptation yields one additional service call."""
+ switch, _ = await setup_lights_and_switch(
+ hass,
+ {
+ INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True,
+ CONF_SEPARATE_TURN_ON_COMMANDS: True,
+ },
+ True,
+ )
+
+ _mock_sun_light_settings(
+ switch,
+ {
+ ATTR_BRIGHTNESS_PCT: 67,
+ ATTR_COLOR_TEMP_KELVIN: 3448,
+ },
+ )
+
+ event_context_ids = await _turn_on_and_track_event_contexts(
+ hass, "test_context", ENTITY_LIGHT3
+ )
+
+ # Expect two service calls
+ assert len(event_context_ids) == 2
+ assert event_context_ids[0] == "test_context"
+ assert is_our_context_id(event_context_ids[1])
+
+ # Expect adapted light state
+ state = hass.states.get(ENTITY_LIGHT3)
+ assert state.attributes[ATTR_BRIGHTNESS] == 171
+ assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448
+
+
+async def test_proactive_adaptation_toggle(hass):
+ """Validate that a proactive adaptation updates service calls which toggle a light on,
+ but not those which toggle off.
+
+ This test is based on the fact that contexts of proactive adaptations are recorded.
+ """
+ switch, _ = await setup_lights_and_switch(
+ hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True
+ )
+
+ # Toggle ON
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TOGGLE,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT3},
+ blocking=True,
+ context=Context(id="test1"),
+ )
+
+ assert switch.turn_on_off_listener.is_proactively_adapting("test1")
+
+ # Toggle OFF
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TOGGLE,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT3},
+ blocking=True,
+ context=Context(id="test2"),
+ )
+
+ assert not switch.turn_on_off_listener.is_proactively_adapting("test2")
+
+
+async def test_proactive_adaptation_transition_override(hass):
+ """Validate that transitions in service calls are preferred over the default transition."""
+ switch, (_, _, light3) = await setup_lights_and_switch(
+ hass,
+ {
+ INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True,
+ CONF_INITIAL_TRANSITION: 123,
+ },
+ True,
+ )
+
+ with patch.object(
+ light3, "async_turn_on", wraps=light3.async_turn_on
+ ) as patched_async_turn_on:
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT3},
+ blocking=True,
+ )
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT3, ATTR_TRANSITION: 456},
+ blocking=True,
+ )
+
+ # Assert that default is used when no transition is specified in service call
+ kwargs = patched_async_turn_on.call_args_list[0].kwargs
+ assert set({ATTR_TRANSITION: 123}.items()).issubset(kwargs.items())
+
+ # Assert that specified service call transition takes precedence over default
+ kwargs = patched_async_turn_on.call_args_list[1].kwargs
+ assert set({ATTR_TRANSITION: 456}.items()).issubset(kwargs.items())
+
+ # Cleanup
+ switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3)
From 1cedb3fbc7672f2de7043b08ebd38af8f698ee64 Mon Sep 17 00:00:00 2001
From: Mario Guggenberger
Date: Wed, 19 Jul 2023 18:53:42 +0200
Subject: [PATCH 042/549] chore: bump manifest version to 1.15.0 (#634)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 4bbdd6f3..6cb2cd5a 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.14.0"
+ "version": "1.15.0"
}
From 1133f0defa53040db291f697ab78cbab6d790750 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 20 Jul 2023 08:52:14 -0700
Subject: [PATCH 043/549] Find adaptive_switch in TurnOnOffListener (#635)
* Find adaptive_switch in TurnOnOffListener
* remove unused argument
* Bump to 1.15.1
---
.../adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 19 ++++++++-----------
2 files changed, 9 insertions(+), 12 deletions(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 6cb2cd5a..ffc87e86 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.15.0"
+ "version": "1.15.1"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 1789439d..edbc8214 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -308,7 +308,6 @@ def _get_switches_with_lights(
def find_switch_for_lights(
hass: HomeAssistant,
lights: list[str],
- is_on: bool = False,
) -> AdaptiveSwitch:
"""Find the switch that controls the lights in 'lights'."""
switches = _get_switches_with_lights(hass, lights)
@@ -365,7 +364,7 @@ def _get_switches_from_service_call(
return switches
if lights:
- switch = find_switch_for_lights(hass, lights, service_call)
+ switch = find_switch_for_lights(hass, lights)
return [switch]
raise ValueError(
@@ -459,7 +458,6 @@ async def async_setup_entry(
adapt_color_switch,
adapt_brightness_switch,
)
- turn_on_off_listener.adaptive_switch = switch
# save our switch instance, allows us to make switch's entity_id optional in service calls.
hass.data[DOMAIN][config_entry.entry_id]["instance"] = switch
@@ -1752,7 +1750,6 @@ class TurnOnOffListener:
)
)
- self.adaptive_switch: AdaptiveSwitch | None
self._proactively_adapting_contexts: dict[str, str] = {}
is_proactive_adaptation_enabled = (
@@ -1843,8 +1840,8 @@ class TurnOnOffListener:
return
entity_id = entity_ids[0]
-
- if entity_id not in self.adaptive_switch.lights:
+ adaptive_switch = find_switch_for_lights(self.hass, [entity_id])
+ if entity_id not in adaptive_switch.lights:
return
if self.manual_control.get(entity_id, False):
@@ -1862,14 +1859,14 @@ class TurnOnOffListener:
self.reset(entity_id, reset_manual_control=False)
self.clear_proactively_adapting(entity_id)
- adapt_brightness = self.adaptive_switch.adapt_brightness_switch.is_on or False
- adapt_color = self.adaptive_switch.adapt_color_switch.is_on or False
+ adapt_brightness = adaptive_switch.adapt_brightness_switch.is_on or False
+ adapt_color = adaptive_switch.adapt_color_switch.is_on or False
transition = (
data[CONF_PARAMS].get(ATTR_TRANSITION, None)
- or self.adaptive_switch.initial_transition
+ or adaptive_switch.initial_transition
)
- adaptation_data = await self.adaptive_switch.prepare_adaptation_data(
+ adaptation_data = await adaptive_switch.prepare_adaptation_data(
entity_id,
transition,
adapt_brightness,
@@ -1901,7 +1898,7 @@ class TurnOnOffListener:
self.set_proactively_adapting(adaptation_data.context.id, entity_id)
adaptation_data.initial_sleep = True
asyncio.create_task( # Don't await to avoid blocking the service call
- self.adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data)
+ adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data)
)
def _handle_timer(
From 1b31b2693818b975b978a85361541f97393983fe Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 20 Jul 2023 08:53:11 -0700
Subject: [PATCH 044/549] Add @protyposis to codeowners (#636)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index ffc87e86..40131076 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "adaptive_lighting",
"name": "Adaptive Lighting",
- "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1"],
+ "codeowners": ["@basnijholt", "@RubenKelevra", "@th3w1zard1", "@protyposis"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/basnijholt/adaptive-lighting#readme",
From fd5f6bf3106aa05cbcb94169ad0cdc3759534f9f Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 20 Jul 2023 11:53:38 -0700
Subject: [PATCH 045/549] Pass on lights that are not managed by AL, closes
#638 (#639)
* Pass on lights that are not managed by AL, closes #638
* Rename exception
---
custom_components/adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 15 ++++++++++++---
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 40131076..52f0554c 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.15.1"
+ "version": "1.15.2"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index edbc8214..4cbd6854 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -305,6 +305,10 @@ def _get_switches_with_lights(
return switches
+class NoSwitchFoundError(ValueError):
+ """No switches found for lights."""
+
+
def find_switch_for_lights(
hass: HomeAssistant,
lights: list[str],
@@ -318,13 +322,13 @@ def find_switch_for_lights(
if len(on_switches) == 1:
# Of the multiple switches, only one is on
return on_switches[0]
- raise ValueError(
+ raise NoSwitchFoundError(
f"find_switch_for_lights: Light(s) {lights} found in multiple switch configs"
f" ({[s.entity_id for s in switches]}). You must pass a switch under"
f" 'entity_id'."
)
else:
- raise ValueError(
+ raise NoSwitchFoundError(
f"find_switch_for_lights: Light(s) {lights} not found in any switch's"
f" configuration. You must either include the light(s) that is/are"
f" in the integration config, or pass a switch under 'entity_id'."
@@ -1840,7 +1844,12 @@ class TurnOnOffListener:
return
entity_id = entity_ids[0]
- adaptive_switch = find_switch_for_lights(self.hass, [entity_id])
+ try:
+ adaptive_switch = find_switch_for_lights(self.hass, [entity_id])
+ except NoSwitchFoundError:
+ # This might be a light that is not managed by this AL instance.
+ return
+
if entity_id not in adaptive_switch.lights:
return
From 19d503b4d7e3c317a9254caacadf008c820cc2c5 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 20 Jul 2023 13:59:19 -0700
Subject: [PATCH 046/549] Revert supported_features from #565 and #575, fixes
#601 (#637)
---
custom_components/adaptive_lighting/const.py | 2 -
custom_components/adaptive_lighting/switch.py | 109 ++++++------------
tests/test_switch.py | 84 +-------------
3 files changed, 39 insertions(+), 156 deletions(-)
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index 3aaf9dda..241689fc 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -289,8 +289,6 @@ VALIDATION_TUPLES = [
),
]
-CONST_COLOR = "color"
-
def timedelta_as_int(value):
"""Convert a `datetime.timedelta` object to an integer.
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 4cbd6854..2bc17862 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -18,12 +18,7 @@ import astral
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
- ATTR_HS_COLOR,
- ATTR_MAX_COLOR_TEMP_KELVIN,
- ATTR_MIN_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
- ATTR_RGBW_COLOR,
- ATTR_RGBWW_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
ATTR_XY_COLOR,
@@ -141,7 +136,6 @@ from .const import (
CONF_TRANSITION,
CONF_TURN_ON_LIGHTS,
CONF_USE_DEFAULTS,
- CONST_COLOR,
DOMAIN,
EXTRA_VALIDATION,
ICON_BRIGHTNESS,
@@ -163,21 +157,10 @@ from .const import (
from .hass_utils import setup_service_call_interceptor
_SUPPORT_OPTS = {
- COLOR_MODE_BRIGHTNESS: SUPPORT_BRIGHTNESS,
- COLOR_MODE_COLOR_TEMP: SUPPORT_COLOR_TEMP,
- CONST_COLOR: SUPPORT_COLOR,
- ATTR_TRANSITION: SUPPORT_TRANSITION,
-}
-
-
-VALID_COLOR_MODES = {
- COLOR_MODE_BRIGHTNESS: ATTR_BRIGHTNESS,
- COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP_KELVIN,
- COLOR_MODE_HS: ATTR_HS_COLOR,
- COLOR_MODE_RGB: ATTR_RGB_COLOR,
- COLOR_MODE_RGBW: ATTR_RGBW_COLOR,
- COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR,
- COLOR_MODE_XY: ATTR_XY_COLOR,
+ "brightness": SUPPORT_BRIGHTNESS,
+ "color_temp": SUPPORT_COLOR_TEMP,
+ "color": SUPPORT_COLOR,
+ "transition": SUPPORT_TRANSITION,
}
_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT)
@@ -625,53 +608,36 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
return list(all_lights)
-def _supported_to_attributes(supported):
- supported_attributes = {}
- supports_colors = False
- for mode in supported:
- attr = VALID_COLOR_MODES.get(mode)
- if attr:
- supported_attributes[attr] = True
- if attr in COLOR_ATTRS:
- supports_colors = True
- # ATTR_SUPPORTED_FEATURES only
- elif mode in _SUPPORT_OPTS:
- supported_attributes[mode] = True
- if CONST_COLOR in supported_attributes:
- supports_colors = True
- supported_attributes.pop(CONST_COLOR)
- 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_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ supported = {
+ key for key, value in _SUPPORT_OPTS.items() if 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:
+ if COLOR_MODE_RGB in supported_color_modes:
+ supported.add("color")
# 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
+ supported.add("brightness")
+ if COLOR_MODE_RGBW in supported_color_modes:
+ supported.add("color")
+ supported.add("brightness") # see above url
+ if COLOR_MODE_RGBWW:
+ supported.add("color")
+ supported.add("brightness") # see above url
+ if COLOR_MODE_XY in supported_color_modes:
+ supported.add("color")
+ supported.add("brightness") # see above url
+ if COLOR_MODE_HS in supported_color_modes:
+ supported.add("color")
+ supported.add("brightness") # see above url
+ if COLOR_MODE_COLOR_TEMP in supported_color_modes:
+ supported.add("color_temp")
+ supported.add("brightness") # see above url
+ if COLOR_MODE_BRIGHTNESS in supported_color_modes:
+ supported.add("brightness")
+ return supported
def color_difference_redmean(
@@ -1122,13 +1088,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# Build service data.
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
- use_transition = ATTR_TRANSITION in features and transition > 0
+ use_transition = "transition" in features and transition > 0
if use_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)
service_data[ATTR_BRIGHTNESS] = brightness
@@ -1137,18 +1103,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and self._sun_light_settings.sleep_rgb_or_color_temp == "rgb_color"
)
if (
- ATTR_COLOR_TEMP_KELVIN in features
+ "color_temp" in features
and adapt_color
- and not (prefer_rgb_color and supports_colors)
- and not (sleep_rgb and supports_colors)
+ and not (prefer_rgb_color and "color" in features)
+ and not (sleep_rgb and "color" in features)
):
_LOGGER.debug("%s: Setting color_temp of light %s", self._name, light)
- min_kelvin = features[ATTR_MIN_COLOR_TEMP_KELVIN]
- max_kelvin = features[ATTR_MAX_COLOR_TEMP_KELVIN]
+ attributes = self.hass.states.get(light).attributes
+ 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 = max(min(color_temp_kelvin, max_kelvin), min_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)
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 4ac92c64..2136b707 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -3,23 +3,18 @@
import asyncio
from copy import deepcopy
import datetime
-import itertools
import logging
from random import randint
from typing import Any
-from unittest.mock import MagicMock, Mock, patch
+from unittest.mock import Mock, patch
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
ATTR_COLOR_TEMP_KELVIN,
- ATTR_MAX_COLOR_TEMP_KELVIN,
- ATTR_MIN_COLOR_TEMP_KELVIN,
ATTR_RGB_COLOR,
- ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
ATTR_XY_COLOR,
- COLOR_MODE_BRIGHTNESS,
)
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.light import SERVICE_TURN_OFF
@@ -75,7 +70,6 @@ from custom_components.adaptive_lighting.const import (
CONF_TRANSITION,
CONF_TURN_ON_LIGHTS,
CONF_USE_DEFAULTS,
- CONST_COLOR,
DEFAULT_MAX_BRIGHTNESS,
DEFAULT_NAME,
DEFAULT_SLEEP_BRIGHTNESS,
@@ -88,12 +82,9 @@ from custom_components.adaptive_lighting.const import (
UNDO_UPDATE_LISTENER,
)
from custom_components.adaptive_lighting.switch import (
- _SUPPORT_OPTS,
INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION,
- VALID_COLOR_MODES,
AdaptiveSwitch,
_attributes_have_changed,
- _supported_features,
color_difference_redmean,
create_context,
is_our_context,
@@ -559,79 +550,6 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass):
assert light not in switch.turn_on_off_listener.lights
-def test_supported_features(hass): # noqa: C901
- """Test the supported features of a light."""
-
- possible_legacy_features = {}
- MAX_COMBINATIONS = 4 # maximum number of elements that can be combined
- for i in range(1, min(MAX_COMBINATIONS, len(_SUPPORT_OPTS)) + 1):
- for combination in itertools.combinations(_SUPPORT_OPTS.keys(), i):
- key = "_".join(combination)
- value = [v for k, v in _SUPPORT_OPTS.items() if k in combination]
- possible_legacy_features[key] = value
-
- possible_color_modes = {}
- for i in range(1, len(VALID_COLOR_MODES) + 1):
- for combination in itertools.combinations(VALID_COLOR_MODES.keys(), i):
- key = "_".join(combination)
- value = [v for k, v in VALID_COLOR_MODES.items() if k in combination]
- possible_color_modes[key] = value
-
- # create a mock HomeAssistant object
- hass = MagicMock()
-
- # iterate over possible legacy features
- for feature_key, feature_values in possible_legacy_features.items():
- # _LOGGER.debug(feature_values)
- # set the attributes of the mock state object to the possible legacy feature values
- state_attrs = {ATTR_SUPPORTED_FEATURES: sum(feature_values)}
- hass.states.get.return_value.attributes = state_attrs
-
- # iterate over possible color modes
- for mode_key, mode_values in possible_color_modes.items():
- # _LOGGER.debug(mode_values)
- # set the attributes of the mock state object to the possible color mode values
- state_attrs[ATTR_SUPPORTED_COLOR_MODES] = set(mode_values)
- hass.states.get.return_value.attributes = state_attrs
-
- # Handle both the new and the old _supported_features.
- result = _supported_features(hass, ENTITY_LIGHT)
- supported, supports_colors = (
- result if isinstance(result, tuple) else (result, None)
- )
- expected_supported = {} if supports_colors is not None else set()
- for mode, attr in VALID_COLOR_MODES.items():
- if mode in mode_values:
- if supports_colors is None:
- expected_supported.add(mode)
- else:
- expected_supported[attr] = True
- if supports_colors is True:
- expected_supported[COLOR_MODE_BRIGHTNESS] = True
- for opt, value in _SUPPORT_OPTS.items():
- if value in feature_values:
- if supports_colors is None:
- expected_supported.add(opt)
- else:
- if supports_colors is True:
- expected_supported[COLOR_MODE_BRIGHTNESS] = True
- if opt in VALID_COLOR_MODES:
- expected_supported[VALID_COLOR_MODES[opt]] = True
- elif opt != CONST_COLOR:
- expected_supported[opt] = True
- if ATTR_MIN_COLOR_TEMP_KELVIN in supported:
- supported.pop(ATTR_MIN_COLOR_TEMP_KELVIN)
- if ATTR_MAX_COLOR_TEMP_KELVIN in supported:
- supported.pop(ATTR_MAX_COLOR_TEMP_KELVIN)
- assert supported == expected_supported, (
- f"\nExpected supported: {expected_supported}\n"
- f"Actual supported: {supported}\n"
- f"feature_values: {feature_values}\n"
- f"mode_values: {mode_values}\n"
- f"supports_colors: {supports_colors}\n"
- )
-
-
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_manual_control(hass):
"""Test the 'manual control' tracking."""
From 6c001b413292f265b80c7fc73941fe63402a71fb Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 20 Jul 2023 14:42:36 -0700
Subject: [PATCH 047/549] Update version to 1.16.0 in manifest.json (#640)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 52f0554c..edb27fe8 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.15.2"
+ "version": "1.16.0"
}
From 2575f4ec30dd9fe3b6cf4493530beb8519d940d5 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 20 Jul 2023 17:12:41 -0700
Subject: [PATCH 048/549] Only adapt if switch if on in
_service_interceptor_turn_on_handler (#642)
* Do not adapt if switch is off in _service_interceptor_turn_on_handler
* Bump to 1.16.1
---
custom_components/adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 3 +++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index edb27fe8..91be72a6 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.16.0"
+ "version": "1.16.1"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 2bc17862..e5a031c6 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1817,6 +1817,9 @@ class TurnOnOffListener:
# This might be a light that is not managed by this AL instance.
return
+ if not adaptive_switch.is_on:
+ return
+
if entity_id not in adaptive_switch.lights:
return
From 49857632cc2184d5cac2bf717631a2427481f61a Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 21 Jul 2023 14:46:18 -0700
Subject: [PATCH 049/549] Fix multiple switches controlling one light (reactive
path) (#644)
* Fix multiple switches controlling one light
* WIP
* Keep length in AdaptationData
* Do not proceed if there is nothing to do
* Simplify
* Set tasks correctly
* log more
* WIP
* realize that intercept and double switch not possible
* Remove which from find_switch_for_lights
* Rephrase
* Check which one to cancel
* length -> max_length
* Implement test_two_switches_for_single_light
* Clean up
* Simplify is_color_brightness_or_both
* Improve test
* no wait
* logging
* just block
* make test such that it fails on main
---
.../adaptive_lighting/adaptation_utils.py | 45 ++++++++++-
custom_components/adaptive_lighting/switch.py | 81 ++++++++++++++++---
tests/test_switch.py | 76 ++++++++++++++++-
3 files changed, 183 insertions(+), 19 deletions(-)
diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py
index 599805a6..a5f33a30 100644
--- a/custom_components/adaptive_lighting/adaptation_utils.py
+++ b/custom_components/adaptive_lighting/adaptation_utils.py
@@ -1,7 +1,8 @@
"""Utility functions for adaptation commands."""
from collections.abc import AsyncGenerator
from dataclasses import dataclass
-from typing import Any
+import logging
+from typing import Any, Literal
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -18,6 +19,8 @@ from homeassistant.components.light import (
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import Context, HomeAssistant, State
+_LOGGER = logging.getLogger(__name__)
+
COLOR_ATTRS = { # Should ATTR_PROFILE be in here?
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN,
@@ -98,7 +101,7 @@ def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool:
async def _create_service_call_data_iterator(
hass: HomeAssistant,
service_datas: list[ServiceData],
- filter_by_state: bool = False,
+ filter_by_state: bool,
) -> AsyncGenerator[ServiceData, None]:
"""Enumerates and filters a list of service datas on the fly.
@@ -133,6 +136,8 @@ class AdaptationData:
context: Context
sleep_time: float
service_call_datas: AsyncGenerator[ServiceData, None]
+ max_length: int
+ which: Literal["brightness", "color", "both"]
initial_sleep: bool = False
async def next_service_call_data(self) -> ServiceData | None:
@@ -140,6 +145,26 @@ class AdaptationData:
return await anext(self.service_call_datas, None)
+class NoColorOrBrightnessInServiceData(Exception):
+ """Exception raised when no color or brightness attributes are found in service data."""
+
+
+def is_color_brightness_or_both(
+ service_data: ServiceData,
+) -> Literal["brightness", "color", "both"]:
+ """Extract the 'which' attribute from the service data."""
+ has_brightness = ATTR_BRIGHTNESS in service_data
+ has_color = any(attr in service_data for attr in COLOR_ATTRS)
+ if has_brightness and has_color:
+ return "both"
+ if has_brightness:
+ return "brightness"
+ if has_color:
+ return "color"
+ msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}"
+ raise NoColorOrBrightnessInServiceData(msg)
+
+
def prepare_adaptation_data(
hass: HomeAssistant,
entity_id: str,
@@ -150,7 +175,12 @@ def prepare_adaptation_data(
split: bool,
filter_by_state: bool,
) -> AdaptationData:
- "Prepares a data object carrying all data required to execute an adaptation."
+ """Prepares a data object carrying all data required to execute an adaptation."""
+ _LOGGER.debug(
+ "Preparing adaptation data for %s with service data %s",
+ entity_id,
+ service_data,
+ )
service_datas = (
[service_data] if not split else _split_service_call_data(service_data)
)
@@ -163,4 +193,11 @@ def prepare_adaptation_data(
hass, service_datas, filter_by_state
)
- return AdaptationData(entity_id, context, sleep_time, service_data_iterator)
+ return AdaptationData(
+ entity_id,
+ context,
+ sleep_time=sleep_time,
+ service_call_datas=service_data_iterator,
+ max_length=len(service_datas),
+ which=is_color_brightness_or_both(service_data),
+ )
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index e5a031c6..b07a3c8e 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1071,7 +1071,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
adapt_color: bool | None = None,
prefer_rgb_color: bool | None = None,
context: Context | None = None,
- ) -> AdaptationData:
+ ) -> AdaptationData | None:
if transition is None:
transition = self._transition
if adapt_brightness is None:
@@ -1081,6 +1081,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if prefer_rgb_color is None:
prefer_rgb_color = self._prefer_rgb_color
+ if not adapt_color and not adapt_brightness:
+ _LOGGER.debug(
+ "%s: Skipping adaptation of %s because both adapt_brightness and"
+ " adapt_color are False",
+ self._name,
+ light,
+ )
+ return None
+
# The switch might be off and not have _settings set.
self._settings = self._sun_light_settings.get_settings(
self.sleep_mode_switch.is_on, transition
@@ -1150,7 +1159,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if self.turn_on_off_listener.is_proactively_adapting(context.parent_id):
# Skip if adaptation was already executed by the service call interceptor
- _LOGGER.debug("Skipping reactive adaptation of %s", context.parent_id)
+ _LOGGER.debug(
+ "%s: Skipping reactive adaptation of %s", self._name, context.parent_id
+ )
return
data = await self.prepare_adaptation_data(
@@ -1161,6 +1172,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
prefer_rgb_color,
context,
)
+ if data is None:
+ return None # nothing to adapt
await self.execute_cancellable_adaptation_calls(data)
@@ -1209,15 +1222,32 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
to cancel an ongoing adaptation when a light is turned off.
"""
# Prevent overlap of multiple adaptation sequences
- self.turn_on_off_listener.cancel_ongoing_adaptation_calls(data.entity_id)
-
+ listener = self.turn_on_off_listener
+ listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which)
+ _LOGGER.debug(
+ "%s: execute_cancellable_adaptation_calls with data: %s"
+ "adaptation_tasks_brightness: %s"
+ "adaptation_tasks_color: %s",
+ self._name,
+ data,
+ listener.adaptation_tasks_brightness,
+ listener.adaptation_tasks_color,
+ )
# Execute adaptation calls within a task
try:
task = asyncio.ensure_future(self._execute_adaptation_calls(data))
- self.turn_on_off_listener.adaptation_tasks[data.entity_id] = task
+ if data.which in ("both", "brightness"):
+ listener.adaptation_tasks_brightness[data.entity_id] = task
+ if data.which in ("both", "color"):
+ listener.adaptation_tasks_color[data.entity_id] = task
await task
except asyncio.CancelledError:
- _LOGGER.debug("Ongoing adaptation of %s cancelled", data.entity_id)
+ _LOGGER.debug(
+ "%s: Ongoing adaptation of %s cancelled, with AdaptationData: %s",
+ self._name,
+ data.entity_id,
+ data,
+ )
async def _update_attrs_and_maybe_adapt_lights(
self,
@@ -1699,7 +1729,8 @@ class TurnOnOffListener:
# Track last 'service_data' to 'light.turn_on' resulting from this integration
self.last_service_data: dict[str, dict[str, Any]] = {}
# Track ongoing split adaptations to be able to cancel them
- self.adaptation_tasks: dict[str, asyncio.Task] = {}
+ self.adaptation_tasks_brightness: dict[str, asyncio.Task] = {}
+ self.adaptation_tasks_color: dict[str, asyncio.Task] = {}
# Track auto reset of manual_control
self.auto_reset_manual_control_timers: dict[str, _AsyncSingleShotTimer] = {}
@@ -1815,6 +1846,11 @@ class TurnOnOffListener:
adaptive_switch = find_switch_for_lights(self.hass, [entity_id])
except NoSwitchFoundError:
# This might be a light that is not managed by this AL instance.
+ _LOGGER.debug(
+ "No (or multiple) adaptive switch(es) found for entity %s,"
+ " skipping adaptation by intercepting service call",
+ entity_id,
+ )
return
if not adaptive_switch.is_on:
@@ -1851,6 +1887,8 @@ class TurnOnOffListener:
adapt_brightness,
adapt_color,
)
+ # if adaptation_data is None:
+ # return
# Take first adaptation item to apply it to this service call
first_service_data = await adaptation_data.next_service_call_data()
@@ -1970,10 +2008,31 @@ class TurnOnOffListener:
self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset)
- def cancel_ongoing_adaptation_calls(self, light_id: str):
- """Cancels an ongoing sequence of adaptation service calls for a specific light entity."""
- if (previous_task := self.adaptation_tasks.get(light_id)) is not None:
- previous_task.cancel()
+ def cancel_ongoing_adaptation_calls(
+ self, light_id: str, which: Literal["color", "brightness", "both"] = "both"
+ ):
+ """Cancel ongoing adaptation service calls for a specific light entity."""
+ brightness_task = self.adaptation_tasks_brightness.get(light_id)
+ color_task = self.adaptation_tasks_color.get(light_id)
+ if which in ("both", "brightness") and brightness_task is not None:
+ _LOGGER.debug(
+ "Cancelled ongoing brightness adaptation calls (%s) for '%s'",
+ brightness_task,
+ light_id,
+ )
+ brightness_task.cancel()
+ if (
+ which in ("both", "color")
+ and color_task is not None
+ and color_task is not brightness_task
+ ):
+ _LOGGER.debug(
+ "Cancelled ongoing color adaptation calls (%s) for '%s'",
+ color_task,
+ light_id,
+ )
+ # color_task might be the same as brightness_task
+ color_task.cancel()
def reset(self, *lights, reset_manual_control=True) -> None:
"""Reset the 'manual_control' status of the lights."""
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 2136b707..0c42b74d 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -1351,7 +1351,9 @@ async def test_cancellable_service_calls_task(hass):
_, switch = await setup_switch(hass, {CONF_SEPARATE_TURN_ON_COMMANDS: True})
context = switch.create_context("test")
- assert switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id) is None
+ assert (
+ switch.turn_on_off_listener.adaptation_tasks_color.get(light.entity_id) is None
+ )
service_data = {
ATTR_BRIGHTNESS: 10,
@@ -1362,11 +1364,15 @@ async def test_cancellable_service_calls_task(hass):
light.entity_id,
context,
0,
- _create_service_call_data_iterator(hass, [service_data]),
+ _create_service_call_data_iterator(hass, [service_data], False),
+ max_length=1,
+ which="both",
)
await switch.execute_cancellable_adaptation_calls(adaptation_data)
- task = switch.turn_on_off_listener.adaptation_tasks.get(light.entity_id)
+ task = switch.turn_on_off_listener.adaptation_tasks_brightness.get(light.entity_id)
+ task2 = switch.turn_on_off_listener.adaptation_tasks_color.get(light.entity_id)
+ assert task is task2
assert task is not None
assert task.done()
@@ -1378,7 +1384,7 @@ async def test_service_calls_task_cancellation(hass):
entity_id = "test_id"
task = asyncio.ensure_future(asyncio.sleep(1))
- switch.turn_on_off_listener.adaptation_tasks[entity_id] = task
+ switch.turn_on_off_listener.adaptation_tasks_brightness[entity_id] = task
switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(entity_id)
@@ -1553,3 +1559,65 @@ async def test_proactive_adaptation_transition_override(hass):
# Cleanup
switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3)
+
+
+async def test_two_switches_for_single_light(hass):
+ """Test the case where someone has two switches for a single light.
+
+ One switch for brightness and another for color.
+ """
+ extra_conf = {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}
+ switch1, (light1, *_) = await setup_lights_and_switch(
+ hass, extra_conf | {CONF_NAME: "switch1"}, all_lights=True
+ )
+ switch2, (light2, *_) = await setup_lights_and_switch(
+ hass, extra_conf | {CONF_NAME: "switch2"}, all_lights=True
+ )
+ assert light1 is light2
+
+ # One switch controls brightness the other color
+ await switch1.adapt_color_switch.async_turn_off()
+ await switch2.adapt_brightness_switch.async_turn_off()
+
+ assert switch1.adapt_brightness_switch.is_on
+ assert switch2.adapt_color_switch.is_on
+
+ async def turn_light(state, **kwargs):
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON if state else SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ _LOGGER.debug("Turn light %s, to %s", state, kwargs)
+
+ def increased_brightness():
+ return (light1._attr_brightness + 100) % 255
+
+ def increased_color_temp():
+ return max(
+ (light1._attr_color_temp + 100) % light1.max_color_temp_kelvin,
+ light1.min_color_temp_kelvin,
+ )
+
+ assert light1.is_on
+ await turn_light(True, brightness=increased_brightness())
+ await turn_light(True, color_temp=increased_color_temp())
+
+ attrs = hass.states.get(light1.entity_id).attributes
+ before_brightness = attrs[ATTR_BRIGHTNESS]
+ before_color_temp = attrs[ATTR_COLOR_TEMP_KELVIN]
+
+ # Turn off "light1"
+ await turn_light(False)
+
+ # Turn on "light1"
+ await turn_light(True)
+
+ # Assert that the brightness and color temp have changed
+ attrs = hass.states.get(light1.entity_id).attributes
+ after_brightness = attrs[ATTR_BRIGHTNESS]
+ after_color_temp = attrs[ATTR_COLOR_TEMP_KELVIN]
+ assert before_brightness != after_brightness
+ assert before_color_temp != after_color_temp
From 30a310c514cbd600ddd3c6771210ddd0dc1ffdf0 Mon Sep 17 00:00:00 2001
From: Mario Guggenberger
Date: Fri, 21 Jul 2023 23:54:38 +0200
Subject: [PATCH 050/549] fix: overlapping adaptations (#646)
Co-authored-by: Bas Nijholt
---
custom_components/adaptive_lighting/switch.py | 42 ++++++++++++++++---
1 file changed, 37 insertions(+), 5 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index b07a3c8e..0e0da07f 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -382,6 +382,8 @@ async def handle_change_switch_settings(
defaults=defaults,
)
+ switch._update_time_interval_listener()
+
_LOGGER.debug(
"Called 'adaptive_lighting.change_switch_settings' service with '%s'",
data,
@@ -787,7 +789,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
data = validate(config_entry)
self._name = data[CONF_NAME]
- self._interval = data[CONF_INTERVAL]
+ self._interval: timedelta = data[CONF_INTERVAL]
self.lights: list[str] = data[CONF_LIGHTS]
# backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS
@@ -815,6 +817,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# Set and unset tracker in async_turn_on and async_turn_off
self.remove_listeners = []
+ self.remove_interval: Callable[[], None] = lambda: None
+
_LOGGER.debug(
"%s: Setting up with '%s',"
" config_entry.data: '%s',"
@@ -961,16 +965,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
assert not self.remove_listeners
- remove_interval = async_track_time_interval(
- self.hass, self._async_update_at_interval, self._interval
- )
+ self._update_time_interval_listener()
+
remove_sleep = async_track_state_change_event(
self.hass,
self.sleep_mode_switch.entity_id,
self._sleep_mode_switch_state_event,
)
- self.remove_listeners.extend([remove_interval, remove_sleep])
+ self.remove_listeners.append(remove_sleep)
if self.lights:
self._expand_light_groups()
@@ -979,7 +982,36 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
self.remove_listeners.append(remove_state)
+ def _update_time_interval_listener(self) -> None:
+ """Create or recreate the adaptation interval listener.
+
+ Recreation is necessary when the configuration has changed (e.g., `send_split_delay`).
+ """
+ self._remove_interval_listener()
+
+ # An adaptation takes a little longer than its nominal duration due processing overhead,
+ # so we factor this in to avoid overlapping adaptations. Since this is a constant value,
+ # it might not cover all cases, but if large enough, it covers most.
+ # Ideally, the interval and adaptation are a coupled process where a finished adaptation
+ # triggers the next, but that requires a larger architectural change.
+ processing_overhead_time = 0.5
+
+ adaptation_interval = (
+ self._interval
+ + timedelta(milliseconds=self._send_split_delay)
+ + timedelta(seconds=processing_overhead_time)
+ )
+
+ self.remove_interval = async_track_time_interval(
+ self.hass, self._async_update_at_interval, adaptation_interval
+ )
+
+ def _remove_interval_listener(self) -> None:
+ self.remove_interval()
+
def _remove_listeners(self) -> None:
+ self._remove_interval_listener()
+
while self.remove_listeners:
remove_listener = self.remove_listeners.pop()
remove_listener()
From 52c7e4b1d5abfb0bf07cc919be027fb7120b445b Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 21 Jul 2023 14:56:44 -0700
Subject: [PATCH 051/549] Bump to v1.16.2 in manifest.json (#647)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 91be72a6..5e121064 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.16.1"
+ "version": "1.16.2"
}
From db40b9af6b86fd42b8d1317a7f582f9cae807137 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 21 Jul 2023 18:08:43 -0700
Subject: [PATCH 052/549] Only build Docker image on main (#648)
---
.github/workflows/docker-build.yml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
index 7326175a..1f6099d0 100644
--- a/.github/workflows/docker-build.yml
+++ b/.github/workflows/docker-build.yml
@@ -3,8 +3,7 @@ name: docker
on:
push:
branches:
- - "master"
- pull_request:
+ - "main"
jobs:
docker:
From fee90250eef694d62e9fbafbc1b07d53741e9e82 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 22 Jul 2023 12:34:20 -0700
Subject: [PATCH 053/549] Better logging and prevent KeyError in
state_changed_event_listener (#650)
* Better logging
* Remove useless and erroneous logging statement
* Bump to 1.16.3
---
.../adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 24 +++++++------------
2 files changed, 10 insertions(+), 16 deletions(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 5e121064..fcf644cd 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.16.2"
+ "version": "1.16.3"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 0e0da07f..fbf6c79c 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1212,10 +1212,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
async def _execute_adaptation_calls(self, data: AdaptationData):
"""Executes a sequence of adaptation service calls for the given service datas."""
- index = 0
- while True:
+ for index in range(data.max_length):
is_first_call = index == 0
- index += 1
# Sleep between multiple service calls.
if not is_first_call or data.initial_sleep:
@@ -1257,13 +1255,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
listener = self.turn_on_off_listener
listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which)
_LOGGER.debug(
- "%s: execute_cancellable_adaptation_calls with data: %s"
- "adaptation_tasks_brightness: %s"
- "adaptation_tasks_color: %s",
+ "%s: execute_cancellable_adaptation_calls with data: %s",
self._name,
data,
- listener.adaptation_tasks_brightness,
- listener.adaptation_tasks_color,
)
# Execute adaptation calls within a task
try:
@@ -1290,9 +1284,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
) -> None:
assert context is not None
_LOGGER.debug(
- "%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'",
+ "%s: '_update_attrs_and_maybe_adapt_lights' called with context.id='%s'"
+ " lights: '%s', transition: '%s', force: '%s'",
self._name,
context.id,
+ lights,
+ transition,
+ force,
)
assert self.is_on
self._settings.update(
@@ -1919,8 +1917,8 @@ class TurnOnOffListener:
adapt_brightness,
adapt_color,
)
- # if adaptation_data is None:
- # return
+ if adaptation_data is None:
+ return
# Take first adaptation item to apply it to this service call
first_service_data = await adaptation_data.next_service_call_data()
@@ -2193,10 +2191,6 @@ class TurnOnOffListener:
entity_id,
)
self.last_state_change[entity_id] = [new_state]
- _LOGGER.debug(
- "Last transition: %s",
- self.last_service_data[entity_id].get(ATTR_TRANSITION),
- )
self.start_transition_timer(entity_id)
elif old_state is not None:
self.last_state_change[entity_id].append(new_state)
From 69592938dbbb29dcdc6f43d6a756409aeb838913 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 22 Jul 2023 17:57:49 -0700
Subject: [PATCH 054/549] Rename TurnOnOffListener to AdaptiveLightingManager
(#654)
* Rename TurnOnOffListener to AdaptiveLightingManager
* Bump Python version
---
.github/workflows/update-readme.yml | 2 +-
.../adaptive_lighting/__init__.py | 8 +-
custom_components/adaptive_lighting/const.py | 2 +-
custom_components/adaptive_lighting/switch.py | 87 ++++++++----------
tests/test_switch.py | 90 +++++++++----------
5 files changed, 85 insertions(+), 104 deletions(-)
diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml
index 2c2754fb..a31d556e 100644
--- a/.github/workflows/update-readme.yml
+++ b/.github/workflows/update-readme.yml
@@ -20,7 +20,7 @@ jobs:
- name: Install Home Assistant
uses: ./.github/workflows/install_dependencies
with:
- python-version: "3.10"
+ python-version: "3.11"
- name: Install markdown-code-runner and README code dependencies
run: |
diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py
index f985e8c5..c4187fa7 100755
--- a/custom_components/adaptive_lighting/__init__.py
+++ b/custom_components/adaptive_lighting/__init__.py
@@ -10,7 +10,7 @@ import voluptuous as vol
from .const import (
_DOMAIN_SCHEMA,
- ATTR_TURN_ON_OFF_LISTENER,
+ ATTR_ADAPTIVE_LIGHTING_MANAGER,
CONF_NAME,
DOMAIN,
UNDO_UPDATE_LISTENER,
@@ -86,10 +86,10 @@ async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool:
if unload_ok:
data.pop(config_entry.entry_id)
- if len(data) == 1 and ATTR_TURN_ON_OFF_LISTENER in data:
+ if len(data) == 1 and ATTR_ADAPTIVE_LIGHTING_MANAGER in data:
# no more config_entries
- turn_on_off_listener = data.pop(ATTR_TURN_ON_OFF_LISTENER)
- turn_on_off_listener.disable()
+ manager = data.pop(ATTR_ADAPTIVE_LIGHTING_MANAGER)
+ manager.disable()
if not data:
hass.data.pop(DOMAIN)
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index 241689fc..15ce097b 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -190,7 +190,7 @@ DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = (
SLEEP_MODE_SWITCH = "sleep_mode_switch"
ADAPT_COLOR_SWITCH = "adapt_color_switch"
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
-ATTR_TURN_ON_OFF_LISTENER = "turn_on_off_listener"
+ATTR_ADAPTIVE_LIGHTING_MANAGER = "manager"
UNDO_UPDATE_LISTENER = "undo_update_listener"
NONE_STR = "None"
ATTR_ADAPT_COLOR = "adapt_color"
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index fbf6c79c..7deddcd8 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -102,7 +102,7 @@ from .const import (
ADAPT_COLOR_SWITCH,
ATTR_ADAPT_BRIGHTNESS,
ATTR_ADAPT_COLOR,
- ATTR_TURN_ON_OFF_LISTENER,
+ ATTR_ADAPTIVE_LIGHTING_MANAGER,
CONF_ADAPT_DELAY,
CONF_ADAPT_UNTIL_SLEEP,
CONF_AUTORESET_CONTROL,
@@ -390,7 +390,7 @@ async def handle_change_switch_settings(
)
all_lights = switch.lights # pylint: disable=protected-access
- switch.turn_on_off_listener.reset(*all_lights, reset_manual_control=False)
+ switch.manager.reset(*all_lights, reset_manual_control=False)
if switch.is_on:
await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access
all_lights,
@@ -412,7 +412,7 @@ def _fire_manual_control_event(
switch.entity_id,
light,
)
- switch.turn_on_off_listener.mark_as_manual_control(light)
+ switch.manager.mark_as_manual_control(light)
fire(
f"{DOMAIN}.manual_control",
{ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id},
@@ -427,9 +427,11 @@ async def async_setup_entry(
data = hass.data[DOMAIN]
assert config_entry.entry_id in data
- if ATTR_TURN_ON_OFF_LISTENER not in data:
- data[ATTR_TURN_ON_OFF_LISTENER] = TurnOnOffListener(hass, config_entry)
- turn_on_off_listener: TurnOnOffListener = data[ATTR_TURN_ON_OFF_LISTENER]
+ if ATTR_ADAPTIVE_LIGHTING_MANAGER not in data:
+ data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = AdaptiveLightingManager(
+ hass, config_entry
+ )
+ manager: AdaptiveLightingManager = data[ATTR_ADAPTIVE_LIGHTING_MANAGER]
sleep_mode_switch = SimpleSwitch(
"Sleep Mode", False, hass, config_entry, ICON_SLEEP
)
@@ -442,7 +444,7 @@ async def async_setup_entry(
switch = AdaptiveSwitch(
hass,
config_entry,
- turn_on_off_listener,
+ manager,
sleep_mode_switch,
adapt_color_switch,
adapt_brightness_switch,
@@ -476,7 +478,7 @@ async def async_setup_entry(
all_lights = switch.lights
else:
all_lights = _expand_light_groups(switch.hass, lights)
- switch.turn_on_off_listener.lights.update(all_lights)
+ switch.manager.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
@@ -509,7 +511,7 @@ async def async_setup_entry(
for light in all_lights:
_fire_manual_control_event(switch, light, service_call.context)
else:
- switch.turn_on_off_listener.reset(*all_lights)
+ switch.manager.reset(*all_lights)
if switch.is_on:
# pylint: disable=protected-access
await switch._update_attrs_and_maybe_adapt_lights(
@@ -594,7 +596,7 @@ def match_switch_state_event(event: Event, from_or_to_state: list[str]):
def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
all_lights = set()
- turn_on_off_listener = hass.data[DOMAIN][ATTR_TURN_ON_OFF_LISTENER]
+ manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER]
for light in lights:
state = hass.states.get(light)
if state is None:
@@ -602,7 +604,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
all_lights.add(light)
elif "entity_id" in state.attributes: # it's a light group
group = state.attributes["entity_id"]
- turn_on_off_listener.lights.discard(light)
+ manager.lights.discard(light)
all_lights.update(group)
_LOGGER.debug("Expanded %s to %s", light, group)
else:
@@ -773,7 +775,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self,
hass,
config_entry: ConfigEntry,
- turn_on_off_listener: TurnOnOffListener,
+ manager: AdaptiveLightingManager,
sleep_mode_switch: SimpleSwitch,
adapt_color_switch: SimpleSwitch,
adapt_brightness_switch: SimpleSwitch,
@@ -781,7 +783,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
"""Initialize the Adaptive Lighting switch."""
# Set attributes that can't be modified during runtime
self.hass = hass
- self.turn_on_off_listener = turn_on_off_listener
+ self.manager = manager
self.sleep_mode_switch = sleep_mode_switch
self.adapt_color_switch = adapt_color_switch
self.adapt_brightness_switch = adapt_brightness_switch
@@ -951,8 +953,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
def _expand_light_groups(self) -> None:
all_lights = _expand_light_groups(self.hass, self.lights)
- self.turn_on_off_listener.lights.update(all_lights)
- self.turn_on_off_listener.set_auto_reset_manual_control_times(
+ self.manager.lights.update(all_lights)
+ self.manager.set_auto_reset_manual_control_times(
all_lights, self._auto_reset_manual_control_time
)
self.lights = list(all_lights)
@@ -1030,12 +1032,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
extra_state_attributes[key] = None
return extra_state_attributes
extra_state_attributes["manual_control"] = [
- light
- for light in self.lights
- if self.turn_on_off_listener.manual_control.get(light)
+ light for light in self.lights if self.manager.manual_control.get(light)
]
extra_state_attributes.update(self._settings)
- timers = self.turn_on_off_listener.auto_reset_manual_control_timers
+ timers = self.manager.auto_reset_manual_control_timers
extra_state_attributes["autoreset_time_remaining"] = {
light: time
for light in self.lights
@@ -1047,16 +1047,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self, which: str = "default", parent: Context | None = None
) -> Context:
"""Create a context that identifies this Adaptive Lighting instance."""
- # Right now the highest number of each context_id it can create is
- # 'adapt_lgt:XXXX:turn_on:*************'
- # 'adapt_lgt:XXXX:interval:************'
- # 'adapt_lgt:XXXX:adapt_lights:********'
- # 'adapt_lgt:XXXX:sleep:***************'
- # 'adapt_lgt:XXXX:light_event:*********'
- # 'adapt_lgt:XXXX:service:*************'
- # The smallest space we have is for adapt_lights, which has
- # 8 characters. In base85 encoding, that's enough space to hold values
- # up to 2**48 - 1, which should give us plenty of calls before we wrap.
context = create_context(self._name, which, self._context_cnt, parent=parent)
self._context_cnt += 1
return context
@@ -1071,7 +1061,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if self.is_on:
return
self._state = True
- self.turn_on_off_listener.reset(*self.lights)
+ self.manager.reset(*self.lights)
await self._setup_listeners()
if adapt_lights:
await self._update_attrs_and_maybe_adapt_lights(
@@ -1086,7 +1076,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
return
self._state = False
self._remove_listeners()
- self.turn_on_off_listener.reset(*self.lights)
+ self.manager.reset(*self.lights)
async def _async_update_at_interval(self, now=None) -> None:
await self._update_attrs_and_maybe_adapt_lights(
@@ -1162,7 +1152,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context = context or self.create_context("adapt_lights")
- self.turn_on_off_listener.last_service_data[light] = service_data
+ self.manager.last_service_data[light] = service_data
return prepare_adaptation_data(
self.hass,
@@ -1189,7 +1179,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
_LOGGER.debug("%s: '%s' is locked", self._name, light)
return
- if self.turn_on_off_listener.is_proactively_adapting(context.parent_id):
+ if self.manager.is_proactively_adapting(context.parent_id):
# Skip if adaptation was already executed by the service call interceptor
_LOGGER.debug(
"%s: Skipping reactive adaptation of %s", self._name, context.parent_id
@@ -1252,7 +1242,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
to cancel an ongoing adaptation when a light is turned off.
"""
# Prevent overlap of multiple adaptation sequences
- listener = self.turn_on_off_listener
+ listener = self.manager
listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which)
_LOGGER.debug(
"%s: execute_cancellable_adaptation_calls with data: %s",
@@ -1309,7 +1299,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
return
for light in lights:
# Don't adapt lights that haven't finished prior transitions.
- timer = self.turn_on_off_listener.transition_timers.get(light)
+ timer = self.manager.transition_timers.get(light)
if timer is not None and timer.is_running():
_LOGGER.debug(
"%s: Light '%s' is still transitioning",
@@ -1352,7 +1342,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if not is_on(self.hass, light):
continue
- manually_controlled = self.turn_on_off_listener.is_manually_controlled(
+ manually_controlled = self.manager.is_manually_controlled(
self,
light,
force,
@@ -1363,7 +1353,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
significant_change = (
self._detect_non_ha_changes
and not force
- and await self.turn_on_off_listener.significant_change(
+ and await self.manager.significant_change(
self,
light,
adapt_brightness,
@@ -1393,7 +1383,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
"%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event
)
# Reset the manually controlled status when the "sleep mode" changes
- self.turn_on_off_listener.reset(*self.lights)
+ self.manager.reset(*self.lights)
await self._update_attrs_and_maybe_adapt_lights(
transition=self._sleep_transition,
force=True,
@@ -1417,13 +1407,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
event.context.id,
)
- if (
- event.context.parent_id
- and not self.turn_on_off_listener.is_proactively_adapting(
- event.context.id
- )
+ if event.context.parent_id and not self.manager.is_proactively_adapting(
+ event.context.id
):
- self.turn_on_off_listener.reset(entity_id, reset_manual_control=False)
+ self.manager.reset(entity_id, reset_manual_control=False)
# Tracks 'off' → 'on' state changes
self._off_to_on_event[entity_id] = event
@@ -1431,7 +1418,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if lock is None:
lock = self._locks[entity_id] = asyncio.Lock()
async with lock:
- if await self.turn_on_off_listener.maybe_cancel_adjusting(
+ if await self.manager.maybe_cancel_adjusting(
entity_id,
off_to_on_event=event,
on_to_off_event=self._on_to_off_event.get(entity_id),
@@ -1471,7 +1458,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
):
# Tracks 'off' → 'on' state changes
self._on_to_off_event[entity_id] = event
- self.turn_on_off_listener.reset(entity_id)
+ self.manager.reset(entity_id)
class SimpleSwitch(SwitchEntity, RestoreEntity):
@@ -1737,11 +1724,11 @@ class SunLightSettings:
}
-class TurnOnOffListener:
+class AdaptiveLightingManager:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
- """Initialize the TurnOnOffListener that is shared among all switches."""
+ """Initialize the AdaptiveLightingManager that is shared among all switches."""
self.hass = hass
data = validate(config_entry)
self.lights = set()
@@ -2177,7 +2164,7 @@ class TurnOnOffListener:
and old_state[0].context.id == new_state.context.id
):
_LOGGER.debug(
- "TurnOnOffListener: State change event of '%s' is already"
+ "AdaptiveLightingManager: State change event of '%s' is already"
" in 'self.last_state_change' (%s)"
" adding this state also",
entity_id,
@@ -2186,7 +2173,7 @@ class TurnOnOffListener:
self.last_state_change[entity_id].append(new_state)
else:
_LOGGER.debug(
- "TurnOnOffListener: New adapt '%s' found for %s",
+ "AdaptiveLightingManager: New adapt '%s' found for %s",
new_state,
entity_id,
)
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 0c42b74d..f356c10a 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -55,7 +55,7 @@ from custom_components.adaptive_lighting.adaptation_utils import (
from custom_components.adaptive_lighting.const import (
ADAPT_BRIGHTNESS_SWITCH,
ADAPT_COLOR_SWITCH,
- ATTR_TURN_ON_OFF_LISTENER,
+ ATTR_ADAPTIVE_LIGHTING_MANAGER,
CONF_AUTORESET_CONTROL,
CONF_DETECT_NON_HA_CHANGES,
CONF_INITIAL_TRANSITION,
@@ -321,7 +321,7 @@ async def test_adaptive_lighting_switches(hass):
ENTITY_ADAPT_COLOR_SWITCH,
ENTITY_ADAPT_BRIGHTNESS_SWITCH,
}
- assert ATTR_TURN_ON_OFF_LISTENER in hass.data[DOMAIN]
+ assert ATTR_ADAPTIVE_LIGHTING_MANAGER in hass.data[DOMAIN]
assert entry.entry_id in hass.data[DOMAIN]
assert len(hass.data[DOMAIN].keys()) == 2
@@ -446,9 +446,7 @@ async def test_light_settings(hass):
assert state.attributes[ATTR_BRIGHTNESS] == round(
255 * switch._settings[ATTR_BRIGHTNESS_PCT] / 100
)
- last_service_data = switch.turn_on_off_listener.last_service_data[
- state.entity_id
- ]
+ last_service_data = switch.manager.last_service_data[state.entity_id]
assert state.attributes[ATTR_BRIGHTNESS] == last_service_data[ATTR_BRIGHTNESS]
assert (
state.attributes[ATTR_COLOR_TEMP_KELVIN]
@@ -483,9 +481,7 @@ async def test_light_settings(hass):
return [hass.states.get(light) for light in lights]
def assert_expected_color_temp(state):
- last_service_data = switch.turn_on_off_listener.last_service_data[
- state.entity_id
- ]
+ last_service_data = switch.manager.last_service_data[state.entity_id]
assert (
state.attributes[ATTR_COLOR_TEMP_KELVIN]
== last_service_data[ATTR_COLOR_TEMP_KELVIN]
@@ -531,7 +527,7 @@ async def test_light_settings(hass):
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
-async def test_turn_on_off_listener_not_tracking_untracked_lights(hass):
+async def test_manager_not_tracking_untracked_lights(hass):
"""Test that lights that are not in a Adaptive Lighting switch aren't tracked."""
switch, _ = await setup_lights_and_switch(hass)
light = "light.kitchen_lights"
@@ -547,7 +543,7 @@ async def test_turn_on_off_listener_not_tracking_untracked_lights(hass):
context=switch.create_context("test")
)
await hass.async_block_till_done()
- assert light not in switch.turn_on_off_listener.lights
+ assert light not in switch.manager.lights
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
@@ -555,7 +551,7 @@ async def test_manual_control(hass):
"""Test the 'manual control' tracking."""
switch, (light, *_) = await setup_lights_and_switch(hass)
context = switch.create_context("test") # needs to be passed to update method
- manual_control = switch.turn_on_off_listener.manual_control
+ manual_control = switch.manager.manual_control
async def update():
await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context)
@@ -707,7 +703,7 @@ async def test_auto_reset_manual_control(hass):
hass, {CONF_AUTORESET_CONTROL: 0.1}
)
context = switch.create_context("test") # needs to be passed to update method
- manual_control = switch.turn_on_off_listener.manual_control
+ manual_control = switch.manager.manual_control
async def update():
await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context)
@@ -846,7 +842,7 @@ async def test_switch_off_on_off(hass):
# Turn light off with transition
await turn_light(False, transition=1)
- assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+ assert not switch.manager.manual_control[ENTITY_LIGHT]
# Set state to on after a second (like happens IRL)
await asyncio.sleep(1e-3)
hass.states.async_set(ENTITY_LIGHT, STATE_ON)
@@ -855,8 +851,8 @@ async def test_switch_off_on_off(hass):
hass.states.async_set(ENTITY_LIGHT, STATE_OFF)
# Now we test whether the sleep task is there
- assert ENTITY_LIGHT in switch.turn_on_off_listener.sleep_tasks
- sleep_task = switch.turn_on_off_listener.sleep_tasks[ENTITY_LIGHT]
+ assert ENTITY_LIGHT in switch.manager.sleep_tasks
+ sleep_task = switch.manager.sleep_tasks[ENTITY_LIGHT]
assert not sleep_task.cancelled()
# A 'light.turn_on' event should cancel that task
@@ -936,7 +932,7 @@ def test_attributes_have_changed():
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_state_change_handlers(hass):
"""
- Test TurnOnOffListener's EVENT_STATE_CHANGED listener.
+ Test AdaptiveLightingManager's EVENT_STATE_CHANGED listener.
======================
Sequence of events:
1. Transition from sleep mode to normal.
@@ -958,7 +954,7 @@ async def test_state_change_handlers(hass):
ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1}
)
await hass.async_block_till_done()
- # Call code in TurnOnOffListener
+ # Call code in AdaptiveLightingManager
hass.bus.async_fire(
EVENT_STATE_CHANGED,
{
@@ -996,10 +992,10 @@ async def test_state_change_handlers(hass):
blocking=True,
)
await hass.async_block_till_done()
- assert switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT)
- assert len(switch.turn_on_off_listener.last_state_change[ENTITY_LIGHT]) == 1
- assert not switch.turn_on_off_listener.transition_timers.get(ENTITY_LIGHT)
- last_service_data = deepcopy(switch.turn_on_off_listener.last_service_data)
+ assert switch.manager.last_state_change.get(ENTITY_LIGHT)
+ assert len(switch.manager.last_state_change[ENTITY_LIGHT]) == 1
+ assert not switch.manager.transition_timers.get(ENTITY_LIGHT)
+ last_service_data = deepcopy(switch.manager.last_service_data)
assert last_service_data.get(ENTITY_LIGHT)
# 2 Adapt from sleep with a 'transition'.
@@ -1008,7 +1004,7 @@ async def test_state_change_handlers(hass):
force=False, transition=0, context=context
)
await hass.async_block_till_done()
- current_service_data = switch.turn_on_off_listener.last_service_data
+ current_service_data = switch.manager.last_service_data
assert current_service_data != last_service_data
for light in switch.lights:
@@ -1029,7 +1025,7 @@ async def test_state_change_handlers(hass):
),
},
)
- assert not switch.turn_on_off_listener.transition_timers.get(light)
+ assert not switch.manager.transition_timers.get(light)
# 2.3 Refire and overwrite the original state_changed event with our 'transition'
hass.bus.async_fire(
@@ -1048,7 +1044,7 @@ async def test_state_change_handlers(hass):
)
await hass.async_block_till_done()
# Assert our transition timer was created.
- assert switch.turn_on_off_listener.transition_timers.get(light)
+ assert switch.manager.transition_timers.get(light)
# 2.5 Simulate a transition. There's no other way to do this in the demo.
events = create_transition_events(
light=light,
@@ -1057,7 +1053,7 @@ async def test_state_change_handlers(hass):
current=current_service_data[light],
total_events=total_events,
)
- # 3. Fire simulated events for our TurnOnOffListener
+ # 3. Fire simulated events for our AdaptiveLightingManager
for event in events:
_LOGGER.debug("Test EVENT_STATE_CHANGED listener")
hass.bus.async_fire(EVENT_STATE_CHANGED, event)
@@ -1065,7 +1061,7 @@ async def test_state_change_handlers(hass):
# On real systems HA fires transition state changes every ~3 seconds.
# asyncio.sleep(3)
# 4. Assert the transition timer started and everything was filled.
- listener = switch.turn_on_off_listener
+ listener = switch.manager
assert listener.last_state_change.get(ENTITY_LIGHT)
assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events
assert listener.transition_timers.get(ENTITY_LIGHT)
@@ -1082,9 +1078,9 @@ async def test_state_change_handlers(hass):
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]
+ assert not switch.manager.manual_control[ENTITY_LIGHT]
await update()
- assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+ assert not switch.manager.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.
@@ -1106,23 +1102,23 @@ async def test_state_change_handlers(hass):
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]
+ assert switch.manager.manual_control[ENTITY_LIGHT]
await update(force=True)
- assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+ assert switch.manager.manual_control[ENTITY_LIGHT]
# turn light off then on should reset manual control.
await turn_light(False)
await turn_light(True)
- assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
+ assert not switch.manager.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]
+ assert switch.manager.last_service_data.get(ENTITY_LIGHT) is not None
+ assert switch.manager.last_state_change.get(ENTITY_LIGHT) is not None
+ assert switch.manager.manual_control[ENTITY_LIGHT]
@pytest.mark.dependency(
@@ -1275,7 +1271,7 @@ async def test_area(hass):
blocking=True,
)
await hass.async_block_till_done()
- assert light.entity_id in switch.turn_on_off_listener.last_service_data
+ assert light.entity_id in switch.manager.last_service_data
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
@@ -1285,10 +1281,10 @@ async def test_area(hass):
await hass.async_block_till_done()
_LOGGER.debug(
- "switch.turn_on_off_listener.last_service_data: %s",
- switch.turn_on_off_listener.last_service_data,
+ "switch.manager.last_service_data: %s",
+ switch.manager.last_service_data,
)
- assert light.entity_id not in switch.turn_on_off_listener.last_service_data
+ assert light.entity_id not in switch.manager.last_service_data
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
@@ -1351,9 +1347,7 @@ async def test_cancellable_service_calls_task(hass):
_, switch = await setup_switch(hass, {CONF_SEPARATE_TURN_ON_COMMANDS: True})
context = switch.create_context("test")
- assert (
- switch.turn_on_off_listener.adaptation_tasks_color.get(light.entity_id) is None
- )
+ assert switch.manager.adaptation_tasks_color.get(light.entity_id) is None
service_data = {
ATTR_BRIGHTNESS: 10,
@@ -1370,8 +1364,8 @@ async def test_cancellable_service_calls_task(hass):
)
await switch.execute_cancellable_adaptation_calls(adaptation_data)
- task = switch.turn_on_off_listener.adaptation_tasks_brightness.get(light.entity_id)
- task2 = switch.turn_on_off_listener.adaptation_tasks_color.get(light.entity_id)
+ task = switch.manager.adaptation_tasks_brightness.get(light.entity_id)
+ task2 = switch.manager.adaptation_tasks_color.get(light.entity_id)
assert task is task2
assert task is not None
assert task.done()
@@ -1384,9 +1378,9 @@ async def test_service_calls_task_cancellation(hass):
entity_id = "test_id"
task = asyncio.ensure_future(asyncio.sleep(1))
- switch.turn_on_off_listener.adaptation_tasks_brightness[entity_id] = task
+ switch.manager.adaptation_tasks_brightness[entity_id] = task
- switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(entity_id)
+ switch.manager.cancel_ongoing_adaptation_calls(entity_id)
try:
await task
@@ -1507,7 +1501,7 @@ async def test_proactive_adaptation_toggle(hass):
context=Context(id="test1"),
)
- assert switch.turn_on_off_listener.is_proactively_adapting("test1")
+ assert switch.manager.is_proactively_adapting("test1")
# Toggle OFF
await hass.services.async_call(
@@ -1518,7 +1512,7 @@ async def test_proactive_adaptation_toggle(hass):
context=Context(id="test2"),
)
- assert not switch.turn_on_off_listener.is_proactively_adapting("test2")
+ assert not switch.manager.is_proactively_adapting("test2")
async def test_proactive_adaptation_transition_override(hass):
@@ -1558,7 +1552,7 @@ async def test_proactive_adaptation_transition_override(hass):
assert set({ATTR_TRANSITION: 456}.items()).issubset(kwargs.items())
# Cleanup
- switch.turn_on_off_listener.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3)
+ switch.manager.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3)
async def test_two_switches_for_single_light(hass):
From c1528ec10a514b7b0e52a1a95f122d9964130961 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 22 Jul 2023 21:19:01 -0700
Subject: [PATCH 055/549] =?UTF-8?q?Refactor,=20simplify=20code,=20rename,?=
=?UTF-8?q?=20and=20set=20minimal=20HA=20core=20=E2=89=A52022.11=20(#655)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Rename TurnOnOffListener to AdaptiveLightingManager
* Bump Python version
* Refactor and unify methods that are called once
* Simplify adaptation_utils.py
* Improve readability in adaptation_utils.py
* More renames
* Simplify
* More renames and simplifications
* fix test
* simplify _supported_features
* rename
* walrus
* drop old astral support
* Only support HA ≥2021.06
* Require 2016.06
* Try 2023.1
* even more old versions
* test
* more versions
* verify that only ≥2022.11 works
* named args
* setdefault
* no astral v1
* var
* simplify
* no need to pass adapt_brightness and adapt_color
* Add comment
---
.github/workflows/pytest.yaml | 6 +
.../adaptive_lighting/adaptation_utils.py | 57 ++--
custom_components/adaptive_lighting/switch.py | 254 +++++++-----------
hacs.json | 3 +-
test_dependencies.py | 36 +--
tests/test_adaptation_utils.py | 12 +-
tests/test_switch.py | 6 +-
7 files changed, 166 insertions(+), 208 deletions(-)
diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml
index b5089ded..ba7eb1a7 100644
--- a/.github/workflows/pytest.yaml
+++ b/.github/workflows/pytest.yaml
@@ -14,6 +14,12 @@ jobs:
fail-fast: false
matrix:
include:
+ - python-version: "3.10"
+ core-version: "2022.11.5"
+ - python-version: "3.10"
+ core-version: "2022.12.9"
+ - python-version: "3.10"
+ core-version: "2023.1.7"
- python-version: "3.10"
core-version: "2023.2.5"
- python-version: "3.10"
diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py
index a5f33a30..374defd1 100644
--- a/custom_components/adaptive_lighting/adaptation_utils.py
+++ b/custom_components/adaptive_lighting/adaptation_utils.py
@@ -61,7 +61,7 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]:
# Distribute the transition duration across all service calls
if service_datas and (transition := service_data.get(ATTR_TRANSITION)) is not None:
- transition = service_data[ATTR_TRANSITION] / len(service_datas)
+ transition /= len(service_datas)
for service_data in service_datas:
service_data[ATTR_TRANSITION] = transition
@@ -69,23 +69,20 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]:
return service_datas
-def _filter_service_data(service_data: ServiceData, state: State | None) -> ServiceData:
+def _remove_redundant_attributes(
+ service_data: ServiceData, state: State
+) -> ServiceData:
"""Filter service data by removing attributes that already equal the given state.
Removes all attributes from service call data whose values are already present
in the target entity's state."""
- if not state:
- return service_data
-
- filtered_service_data = {
- k: service_data[k]
- for k in service_data.keys()
- if k not in state.attributes or service_data[k] != state.attributes[k]
+ return {
+ k: v
+ for k, v in service_data.items()
+ if k not in state.attributes or v != state.attributes[k]
}
- return filtered_service_data
-
def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool:
"""Determines whether the service data justifies an adaptation service call.
@@ -93,9 +90,8 @@ def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool:
A service call is not justified for data which does not contain any entries that
change relevant attributes of an adapting entity, e.g., brightness or color."""
common_attrs = {ATTR_ENTITY_ID, ATTR_TRANSITION}
- relevant_attrs = set(service_data) - common_attrs
- return bool(relevant_attrs)
+ return any(attr not in common_attrs for attr in service_data)
async def _create_service_call_data_iterator(
@@ -118,8 +114,10 @@ async def _create_service_call_data_iterator(
current_entity_state = hass.states.get(entity_id)
# Filter data to remove attributes that equal the current state
- if current_entity_state:
- service_data = _filter_service_data(service_data, current_entity_state)
+ if current_entity_state is not None:
+ service_data = _remove_redundant_attributes(
+ service_data, current_entity_state
+ )
# Emit service data if it still contains relevant attributes (else try next)
if _has_relevant_service_data_attributes(service_data):
@@ -149,7 +147,7 @@ class NoColorOrBrightnessInServiceData(Exception):
"""Exception raised when no color or brightness attributes are found in service data."""
-def is_color_brightness_or_both(
+def _identify_lighting_type(
service_data: ServiceData,
) -> Literal["brightness", "color", "both"]:
"""Extract the 'which' attribute from the service data."""
@@ -181,23 +179,30 @@ def prepare_adaptation_data(
entity_id,
service_data,
)
- service_datas = (
- [service_data] if not split else _split_service_call_data(service_data)
- )
+ if split:
+ service_datas = _split_service_call_data(service_data)
+ else:
+ service_datas = [service_data]
- sleep_time = (
- transition / max(1, len(service_datas)) if transition is not None else 0
- ) + split_delay
+ service_datas_length = len(service_datas)
+
+ if transition is not None:
+ transition_duration_per_data = transition / max(1, service_datas_length)
+ sleep_time = transition_duration_per_data + split_delay
+ else:
+ sleep_time = split_delay
service_data_iterator = _create_service_call_data_iterator(
hass, service_datas, filter_by_state
)
+ lighting_type = _identify_lighting_type(service_data)
+
return AdaptationData(
- entity_id,
- context,
+ entity_id=entity_id,
+ context=context,
sleep_time=sleep_time,
service_call_datas=service_data_iterator,
- max_length=len(service_datas),
- which=is_color_brightness_or_both(service_data),
+ max_length=service_datas_length,
+ which=lighting_type,
)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 7deddcd8..82fd34db 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import base64
import bisect
-from collections.abc import Callable, Coroutine
+from collections.abc import Callable, Coroutine, Iterable
from copy import deepcopy
from dataclasses import dataclass
import datetime
@@ -186,8 +186,7 @@ _DOMAIN_SHORT = "al"
def _int_to_base36(num: int) -> str:
- """
- Convert an integer to its base-36 representation using numbers and uppercase letters.
+ """Convert an integer to its base-36 representation using numbers and uppercase letters.
Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive
alphanumeric representation. The function takes an integer `num` as input and returns
@@ -268,7 +267,7 @@ def is_our_context(context: Context | None) -> bool:
return is_our_context_id(context.id)
-def _get_switches_with_lights(
+def _switches_with_lights(
hass: HomeAssistant, lights: list[str]
) -> list[AdaptiveSwitch]:
"""Get all switches that control at least one of the lights passed."""
@@ -292,12 +291,12 @@ class NoSwitchFoundError(ValueError):
"""No switches found for lights."""
-def find_switch_for_lights(
+def _switch_with_lights(
hass: HomeAssistant,
lights: list[str],
) -> AdaptiveSwitch:
"""Find the switch that controls the lights in 'lights'."""
- switches = _get_switches_with_lights(hass, lights)
+ switches = _switches_with_lights(hass, lights)
if len(switches) == 1:
return switches[0]
elif len(switches) > 1:
@@ -306,13 +305,13 @@ def find_switch_for_lights(
# Of the multiple switches, only one is on
return on_switches[0]
raise NoSwitchFoundError(
- f"find_switch_for_lights: Light(s) {lights} found in multiple switch configs"
+ f"_switch_with_lights: Light(s) {lights} found in multiple switch configs"
f" ({[s.entity_id for s in switches]}). You must pass a switch under"
f" 'entity_id'."
)
else:
raise NoSwitchFoundError(
- f"find_switch_for_lights: Light(s) {lights} not found in any switch's"
+ f"_switch_with_lights: Light(s) {lights} not found in any switch's"
f" configuration. You must either include the light(s) that is/are"
f" in the integration config, or pass a switch under 'entity_id'."
)
@@ -320,7 +319,7 @@ def find_switch_for_lights(
# For documentation on this function, see integration_entities() from HomeAssistant Core:
# https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109
-def _get_switches_from_service_call(
+def _switches_from_service_call(
hass: HomeAssistant, service_call: ServiceCall
) -> list[AdaptiveSwitch]:
data = service_call.data
@@ -351,7 +350,7 @@ def _get_switches_from_service_call(
return switches
if lights:
- switch = find_switch_for_lights(hass, lights)
+ switch = _switch_with_lights(hass, lights)
return [switch]
raise ValueError(
@@ -377,11 +376,7 @@ async def handle_change_switch_settings(
else:
defaults = None
- switch._set_changeable_settings(
- data=data,
- defaults=defaults,
- )
-
+ switch._set_changeable_settings(data=data, defaults=defaults)
switch._update_time_interval_listener()
_LOGGER.debug(
@@ -389,11 +384,10 @@ async def handle_change_switch_settings(
data,
)
- all_lights = switch.lights # pylint: disable=protected-access
- switch.manager.reset(*all_lights, reset_manual_control=False)
+ switch.manager.reset(*switch.lights, reset_manual_control=False)
if switch.is_on:
await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access
- all_lights,
+ switch.lights,
transition=switch.initial_transition,
force=True,
context=switch.create_context("service", parent=service_call.context),
@@ -471,7 +465,7 @@ async def async_setup_entry(
"Called 'adaptive_lighting.apply' service with '%s'",
data,
)
- switches = _get_switches_from_service_call(hass, service_call)
+ switches = _switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
@@ -500,7 +494,7 @@ async def async_setup_entry(
"Called 'adaptive_lighting.set_manual_control' service with '%s'",
data,
)
- switches = _get_switches_from_service_call(hass, service_call)
+ switches = _switches_from_service_call(hass, service_call)
lights = data[CONF_LIGHTS]
for switch in switches:
if not lights:
@@ -528,9 +522,7 @@ async def async_setup_entry(
domain=DOMAIN,
service=SERVICE_APPLY,
service_func=handle_apply,
- schema=apply_service_schema(
- switch.initial_transition
- ), # pylint: disable=protected-access
+ schema=apply_service_schema(switch.initial_transition),
)
# Register `set_manual_control` service
@@ -582,16 +574,15 @@ def validate(
return data
-def match_switch_state_event(event: Event, from_or_to_state: list[str]):
+def _is_state_event(event: Event, from_or_to_state: Iterable[str]):
"""Match state event when either 'from_state' or 'to_state' matches."""
- old_state = event.data.get("old_state")
- from_state_match = old_state is not None and old_state.state in from_or_to_state
-
- new_state = event.data.get("new_state")
- to_state_match = new_state is not None and new_state.state in from_or_to_state
-
- match = from_state_match or to_state_match
- return match
+ return (
+ (old_state := event.data.get("old_state")) is not None
+ and old_state.state in from_or_to_state
+ ) or (
+ (new_state := event.data.get("new_state")) is not None
+ and new_state.state in from_or_to_state
+ )
def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
@@ -612,35 +603,36 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
return list(all_lights)
-def _supported_features(hass: HomeAssistant, light: str):
+def _supported_features(hass: HomeAssistant, light: str) -> set[str]:
state = hass.states.get(light)
supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
supported = {
key for key, value in _SUPPORT_OPTS.items() if supported_features & value
}
+
supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES, set())
- if COLOR_MODE_RGB in supported_color_modes:
- supported.add("color")
- # Adding brightness here, see
- # comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011
- supported.add("brightness")
- if COLOR_MODE_RGBW in supported_color_modes:
- supported.add("color")
- supported.add("brightness") # see above url
- if COLOR_MODE_RGBWW:
- supported.add("color")
- supported.add("brightness") # see above url
- if COLOR_MODE_XY in supported_color_modes:
- supported.add("color")
- supported.add("brightness") # see above url
- if COLOR_MODE_HS in supported_color_modes:
- supported.add("color")
- supported.add("brightness") # see above url
+ color_modes = {
+ COLOR_MODE_RGB,
+ COLOR_MODE_RGBW,
+ COLOR_MODE_RGBWW,
+ COLOR_MODE_XY,
+ COLOR_MODE_HS,
+ }
+
+ # Adding brightness when color mode is supported, see
+ # comment https://github.com/basnijholt/adaptive-lighting/issues/112#issuecomment-836944011
+
+ for mode in color_modes:
+ if mode in supported_color_modes:
+ supported.update({"color", "brightness"})
+ break
+
if COLOR_MODE_COLOR_TEMP in supported_color_modes:
- supported.add("color_temp")
- supported.add("brightness") # see above url
+ supported.update({"color_temp", "brightness"})
+
if COLOR_MODE_BRIGHTNESS in supported_color_modes:
supported.add("brightness")
+
return supported
@@ -670,16 +662,16 @@ def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]:
return attributes
rgb = None
- if ATTR_COLOR_TEMP_KELVIN in attributes:
- rgb = color_temperature_to_rgb(attributes[ATTR_COLOR_TEMP_KELVIN])
- elif ATTR_XY_COLOR in attributes:
- rgb = color_xy_to_RGB(*attributes[ATTR_XY_COLOR])
+ if (color := attributes.get(ATTR_COLOR_TEMP_KELVIN)) is not None:
+ rgb = color_temperature_to_rgb(color)
+ elif (color := attributes.get(ATTR_XY_COLOR)) is not None:
+ rgb = color_xy_to_RGB(*color)
if rgb is not None:
attributes[ATTR_RGB_COLOR] = rgb
_LOGGER.debug(f"Converted {attributes} to rgb {rgb}")
else:
- _LOGGER.debug("No suitable conversion found")
+ _LOGGER.debug("No suitable color conversion found for %s", attributes)
return attributes
@@ -796,10 +788,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# backup data for use in change_switch_settings "configuration" CONF_USE_DEFAULTS
self._config_backup = deepcopy(data)
- self._set_changeable_settings(
- data=data,
- defaults=None,
- )
+ self._set_changeable_settings(data=data, defaults=None)
# Set other attributes
self._icon = ICON_MAIN
@@ -835,7 +824,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
def _set_changeable_settings(
self,
data: dict,
- defaults: dict,
+ defaults: dict | None = None,
):
# Only pass settings users can change during runtime
data = validate(
@@ -853,9 +842,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if self._include_config_in_attributes:
attrdata = deepcopy(data)
for k, v in attrdata.items():
- if isinstance(v, (datetime.date, datetime.datetime)):
+ if isinstance(v, datetime.date | datetime.datetime):
attrdata[k] = v.isoformat()
- if isinstance(v, (datetime.timedelta)):
+ elif isinstance(v, datetime.timedelta):
attrdata[k] = v.total_seconds()
self._config.update(attrdata)
@@ -880,13 +869,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS]
self._expand_light_groups() # updates manual control timers
- _loc = get_astral_location(self.hass)
- if isinstance(_loc, tuple):
- # Astral v2.2
- location, _ = _loc
- else:
- # Astral v1
- location = _loc
+ location, _ = get_astral_location(self.hass)
self._sun_light_settings = SunLightSettings(
name=self._name,
@@ -971,8 +954,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
remove_sleep = async_track_state_change_event(
self.hass,
- self.sleep_mode_switch.entity_id,
- self._sleep_mode_switch_state_event,
+ entity_ids=self.sleep_mode_switch.entity_id,
+ action=self._sleep_mode_switch_state_event_action,
)
self.remove_listeners.append(remove_sleep)
@@ -980,7 +963,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if self.lights:
self._expand_light_groups()
remove_state = async_track_state_change_event(
- self.hass, self.lights, self._light_event
+ self.hass, entity_ids=self.lights, action=self._light_event_action
)
self.remove_listeners.append(remove_state)
@@ -1005,7 +988,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
self.remove_interval = async_track_time_interval(
- self.hass, self._async_update_at_interval, adaptation_interval
+ self.hass,
+ action=self._async_update_at_interval_action,
+ interval=adaptation_interval,
)
def _remove_interval_listener(self) -> None:
@@ -1078,7 +1063,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._remove_listeners()
self.manager.reset(*self.lights)
- async def _async_update_at_interval(self, now=None) -> None:
+ async def _async_update_at_interval_action(self, now=None) -> None:
await self._update_attrs_and_maybe_adapt_lights(
transition=self._transition,
force=False,
@@ -1174,8 +1159,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
prefer_rgb_color: bool | None = None,
context: Context | None = None,
) -> None:
- lock = self._locks.get(light)
- if lock is not None and lock.locked():
+ if (lock := self._locks.get(light)) is not None and lock.locked():
_LOGGER.debug("%s: '%s' is locked", self._name, light)
return
@@ -1293,10 +1277,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if lights is None:
lights = self.lights
- filtered_lights = []
- if not force:
- if self._only_once:
- return
+ if not force and self._only_once:
+ return
+
+ if force:
+ filtered_lights = lights
+ else:
+ filtered_lights = []
for light in lights:
# Don't adapt lights that haven't finished prior transitions.
timer = self.manager.transition_timers.get(light)
@@ -1308,37 +1295,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
else:
filtered_lights.append(light)
- else:
- filtered_lights = lights
if not filtered_lights:
return
- await self._update_manual_control_and_maybe_adapt(
- filtered_lights, transition, force, context
- )
-
- async def _update_manual_control_and_maybe_adapt(
- self,
- lights: list[str],
- transition: int | None,
- force: bool,
- context: Context | None,
- ) -> None:
- assert context is not None
- _LOGGER.debug(
- "%s: '_update_manual_control_and_maybe_adapt(%s, %s, force=%s, context.id=%s)' called",
- self.name,
- lights,
- transition,
- force,
- context.id,
- )
-
adapt_brightness = self.adapt_brightness_switch.is_on
adapt_color = self.adapt_color_switch.is_on
- for light in lights:
+ for light in filtered_lights:
if not is_on(self.hass, light):
continue
@@ -1375,12 +1339,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
else:
await self._adapt_light(light, transition, context=context)
- async def _sleep_mode_switch_state_event(self, event: Event) -> None:
- if not match_switch_state_event(event, (STATE_ON, STATE_OFF)):
+ async def _sleep_mode_switch_state_event_action(self, event: Event) -> None:
+ if not _is_state_event(event, (STATE_ON, STATE_OFF)):
_LOGGER.debug("%s: Ignoring sleep event %s", self._name, event)
return
_LOGGER.debug(
- "%s: _sleep_mode_switch_state_event, event: '%s'", self._name, event
+ "%s: _sleep_mode_switch_state_event_action, event: '%s'", self._name, event
)
# Reset the manually controlled status when the "sleep mode" changes
self.manager.reset(*self.lights)
@@ -1390,7 +1354,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context=self.create_context("sleep", parent=event.context),
)
- async def _light_event(self, event: Event) -> None:
+ async def _light_event_action(self, event: Event) -> None:
old_state = event.data.get("old_state")
new_state = event.data.get("new_state")
entity_id = event.data.get("entity_id")
@@ -1414,9 +1378,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# Tracks 'off' → 'on' state changes
self._off_to_on_event[entity_id] = event
- lock = self._locks.get(entity_id)
- if lock is None:
- lock = self._locks[entity_id] = asyncio.Lock()
+ lock = self._locks.setdefault(entity_id, asyncio.Lock())
async with lock:
if await self.manager.maybe_cancel_adjusting(
entity_id,
@@ -1549,18 +1511,15 @@ class SunLightSettings:
time_zone: datetime.tzinfo
transition: int
- def get_sun_events(self, date: datetime.datetime) -> dict[str, float]:
+ def get_sun_events(self, date: datetime.datetime) -> list[tuple[str, float]]:
"""Get the four sun event's timestamps at 'date'."""
def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime:
time = getattr(self, f"{key}_time")
date_time = datetime.datetime.combine(date, time)
- try: # HA ≤2021.05, https://github.com/basnijholt/adaptive-lighting/issues/128
- utc_time = self.time_zone.localize(date_time).astimezone(dt_util.UTC)
- except AttributeError: # HA ≥2021.06
- utc_time = date_time.replace(
- tzinfo=dt_util.DEFAULT_TIME_ZONE
- ).astimezone(dt_util.UTC)
+ utc_time = date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(
+ dt_util.UTC
+ )
return utc_time
def calculate_noon_and_midnight(
@@ -1606,16 +1565,10 @@ class SunLightSettings:
and self.max_sunrise_time is None
and self.min_sunset_time is None
):
- try:
- # Astral v1
- solar_noon = location.solar_noon(date, local=False)
- solar_midnight = location.solar_midnight(date, local=False)
- except AttributeError:
- # Astral v2
- solar_noon = location.noon(date, local=False)
- solar_midnight = location.midnight(date, local=False)
+ solar_noon = location.noon(date, local=False)
+ solar_midnight = location.midnight(date, local=False)
else:
- (solar_noon, solar_midnight) = calculate_noon_and_midnight(sunset, sunrise)
+ solar_noon, solar_midnight = calculate_noon_and_midnight(sunset, sunrise)
events = [
(SUN_EVENT_SUNRISE, sunrise.timestamp()),
@@ -1641,9 +1594,10 @@ class SunLightSettings:
def relevant_events(self, now: datetime.datetime) -> list[tuple[str, float]]:
"""Get the previous and next sun event."""
events = [
- self.get_sun_events(now + timedelta(days=days)) for days in [-1, 0, 1]
+ event
+ for days in [-1, 0, 1]
+ for event in self.get_sun_events(now + timedelta(days=days))
]
- events = sum(events, []) # flatten lists
events = sorted(events, key=lambda x: x[1])
i_now = bisect.bisect([ts for _, ts in events], now.timestamp())
return events[i_now - 1 : i_now + 1]
@@ -1756,23 +1710,22 @@ class AdaptiveLightingManager:
# Track light transitions
self.transition_timers: dict[str, _AsyncSingleShotTimer] = {}
- self.listener_removers = []
-
- self.listener_removers.append(
+ # Setup listeners and its callbacks to remove them later
+ self.listener_removers = [
self.hass.bus.async_listen(
- EVENT_CALL_SERVICE, self.turn_on_off_event_listener
- )
- )
- self.listener_removers.append(
+ EVENT_CALL_SERVICE,
+ self.turn_on_off_event_listener,
+ ),
self.hass.bus.async_listen(
- EVENT_STATE_CHANGED, self.state_changed_event_listener
- )
- )
+ EVENT_STATE_CHANGED,
+ self.state_changed_event_listener,
+ ),
+ ]
self._proactively_adapting_contexts: dict[str, str] = {}
- is_proactive_adaptation_enabled = (
- data.get(INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, True) is not False
+ is_proactive_adaptation_enabled = data.get(
+ INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, True
)
if is_proactive_adaptation_enabled:
@@ -1860,7 +1813,7 @@ class AdaptiveLightingManager:
entity_id = entity_ids[0]
try:
- adaptive_switch = find_switch_for_lights(self.hass, [entity_id])
+ adaptive_switch = _switch_with_lights(self.hass, [entity_id])
except NoSwitchFoundError:
# This might be a light that is not managed by this AL instance.
_LOGGER.debug(
@@ -1891,18 +1844,13 @@ class AdaptiveLightingManager:
self.reset(entity_id, reset_manual_control=False)
self.clear_proactively_adapting(entity_id)
- adapt_brightness = adaptive_switch.adapt_brightness_switch.is_on or False
- adapt_color = adaptive_switch.adapt_color_switch.is_on or False
- transition = (
- data[CONF_PARAMS].get(ATTR_TRANSITION, None)
- or adaptive_switch.initial_transition
+ transition = data[CONF_PARAMS].get(
+ ATTR_TRANSITION, adaptive_switch.initial_transition
)
adaptation_data = await adaptive_switch.prepare_adaptation_data(
entity_id,
transition,
- adapt_brightness,
- adapt_color,
)
if adaptation_data is None:
return
@@ -1972,7 +1920,7 @@ class AdaptiveLightingManager:
)
async def reset():
- ValueError("TEST")
+ # Called when the timer expires, doesn't need to do anything
_LOGGER.debug(
"Transition finished for light %s",
light,
@@ -2005,7 +1953,7 @@ class AdaptiveLightingManager:
async def reset():
self.reset(light)
- switches = _get_switches_with_lights(self.hass, [light])
+ switches = _switches_with_lights(self.hass, [light])
for switch in switches:
if not switch.is_on:
continue
diff --git a/hacs.json b/hacs.json
index 1a865d2d..f21e6d60 100644
--- a/hacs.json
+++ b/hacs.json
@@ -1,4 +1,5 @@
{
"name": "Adaptive Lighting",
- "render_readme": true
+ "render_readme": true,
+ "homeassistant": "2022.11.0"
}
diff --git a/test_dependencies.py b/test_dependencies.py
index 58b4718f..2cf8dfb0 100644
--- a/test_dependencies.py
+++ b/test_dependencies.py
@@ -1,22 +1,27 @@
+from collections import defaultdict
+
+deps = defaultdict(list)
+components, packages = [], []
+
with open("core/requirements_test_all.txt") as f:
lines = f.readlines()
-components = []
-packages = []
-deps = {}
-for i, line in enumerate(lines):
+for line in lines:
line = line.strip()
+
if line.startswith("# homeassistant."):
- component = line.split("# homeassistant.")[1]
- components.append(component)
+ if components and packages:
+ for component in components:
+ deps[component].extend(packages)
+ components, packages = [], []
+ components.append(line.split("# homeassistant.")[1])
elif components and line:
packages.append(line)
- else:
- for component in components:
- for package in packages:
- deps.setdefault(component, []).append(package)
- components = []
- packages = []
+
+# The last batch of components and packages
+if components and packages:
+ for component in components:
+ deps[component].extend(packages)
required = [
"components.recorder",
@@ -24,10 +29,9 @@ required = [
"components.zeroconf",
"components.http",
"components.stream",
- "components.conversation",
+ "components.conversation", # only available after HA≥2023.2
"components.cloud",
]
-to_install = []
-for r in required:
- to_install.extend(deps[r])
+to_install = [package for r in required for package in deps[r]]
+
print(" ".join(to_install))
diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py
index b1cfcb5b..fc0df739 100644
--- a/tests/test_adaptation_utils.py
+++ b/tests/test_adaptation_utils.py
@@ -14,8 +14,8 @@ import pytest
from custom_components.adaptive_lighting.adaptation_utils import (
ServiceData,
_create_service_call_data_iterator,
- _filter_service_data,
_has_relevant_service_data_attributes,
+ _remove_redundant_attributes,
_split_service_call_data,
prepare_adaptation_data,
)
@@ -74,11 +74,6 @@ async def test_split_service_call_data(input_data, expected_data_list):
@pytest.mark.parametrize(
"service_data,state,service_data_expected",
[
- (
- {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
- None,
- {ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
- ),
(
{ATTR_ENTITY_ID: "light.test", ATTR_BRIGHTNESS: 10, ATTR_TRANSITION: 2},
State("light.test", STATE_ON),
@@ -96,17 +91,16 @@ async def test_split_service_call_data(input_data, expected_data_list):
),
],
ids=[
- "pass all attributes on missing state",
"pass all attributes on empty state",
"remove attributes whose values equal the state",
"keep attributes whose values differ from the state",
],
)
-async def test_filter_service_data(
+async def test_remove_redundant_attributes(
service_data: ServiceData, state: State | None, service_data_expected: ServiceData
):
"""Test filtering of service data."""
- assert _filter_service_data(service_data, state) == service_data_expected
+ assert _remove_redundant_attributes(service_data, state) == service_data_expected
@pytest.mark.parametrize(
diff --git a/tests/test_switch.py b/tests/test_switch.py
index f356c10a..59692f1e 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -1206,10 +1206,10 @@ async def test_turn_on_and_off_when_already_at_that_state(hass):
@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
-async def test_async_update_at_interval(hass):
- """Test '_async_update_at_interval' method."""
+async def test_async_update_at_interval_action(hass):
+ """Test '_async_update_at_interval_action' method."""
_, switch = await setup_switch(hass, {})
- await switch._async_update_at_interval()
+ await switch._async_update_at_interval_action()
@pytest.mark.parametrize("separate_turn_on_commands", (True, False))
From 97f388608ab31d0e95ed6b673533a0599d6b795f Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 23 Jul 2023 13:40:03 -0700
Subject: [PATCH 056/549] With transition_until_sleep and sleep_rgb, after
sunset, use RGB colors (#656)
* With transition_until_sleep and sleep_rgb, after sunset, use RGB colors
Closes #624
* Add comment
* Add test
---
custom_components/adaptive_lighting/switch.py | 44 ++++++++--
tests/test_switch.py | 86 +++++++++++++++++++
2 files changed, 125 insertions(+), 5 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 82fd34db..58fa3eaa 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1123,6 +1123,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and adapt_color
and not (prefer_rgb_color and "color" in features)
and not (sleep_rgb and "color" in features)
+ and not (self._settings["force_rgb_color"] and "color" in features)
):
_LOGGER.debug("%s: Setting color_temp of light %s", self._name, light)
attributes = self.hass.states.get(light).attributes
@@ -1296,9 +1297,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
else:
filtered_lights.append(light)
+ _LOGGER.debug("%s: filtered_lights: '%s'", self._name, filtered_lights)
if not filtered_lights:
return
-
adapt_brightness = self.adapt_brightness_switch.is_on
adapt_color = self.adapt_color_switch.is_on
@@ -1337,6 +1338,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
else:
_fire_manual_control_event(self, light, context)
else:
+ _LOGGER.debug(
+ "%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:"
+ " '%s' with transition %s",
+ self._name,
+ light,
+ transition,
+ )
await self._adapt_light(light, transition, context=context)
async def _sleep_mode_switch_state_event_action(self, event: Event) -> None:
@@ -1487,6 +1495,17 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
self._state = False
+def lerp_color(
+ rgb1: tuple[int, int, int], rgb2: tuple[int, int, int], t: float
+) -> tuple[int, int, int]:
+ """Linearly interpolate between two RGB colors."""
+ return (
+ int(rgb1[0] + t * (rgb2[0] - rgb1[0])),
+ int(rgb1[1] + t * (rgb2[1] - rgb1[1])),
+ int(rgb1[2] + t * (rgb2[2] - rgb1[2])),
+ )
+
+
@dataclass(frozen=True)
class SunLightSettings:
"""Track the state of the sun and associated light settings."""
@@ -1654,15 +1673,29 @@ class SunLightSettings:
if transition is not None
else self.calc_percent(0)
)
+ rgb_color: tuple[float, float, float]
+ # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled)
+ force_rgb_color = False
brightness_pct = self.calc_brightness_pct(percent, is_sleep)
if is_sleep:
color_temp_kelvin = self.sleep_color_temp
- rgb_color: tuple[float, float, float] = self.sleep_rgb_color
+ rgb_color = self.sleep_rgb_color
+ elif (
+ self.sleep_rgb_or_color_temp == "rgb_color"
+ and self.adapt_until_sleep
+ and percent < 0
+ ):
+ # Feature requested in
+ # https://github.com/basnijholt/adaptive-lighting/issues/624
+ # This will result in a perceptible jump in color at sunset and sunrise
+ # because the `color_temperature_to_rgb` function is not 100% accurate.
+ min_color_rgb = color_temperature_to_rgb(self.min_color_temp)
+ rgb_color = lerp_color(min_color_rgb, self.sleep_rgb_color, percent)
+ color_temp_kelvin = self.calc_color_temp_kelvin(percent)
+ force_rgb_color = True
else:
color_temp_kelvin = self.calc_color_temp_kelvin(percent)
- rgb_color: tuple[float, float, float] = color_temperature_to_rgb(
- color_temp_kelvin
- )
+ rgb_color = color_temperature_to_rgb(color_temp_kelvin)
# backwards compatibility for versions < 1.3.1 - see #403
color_temp_mired: float = math.floor(1000000 / color_temp_kelvin)
xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color)
@@ -1675,6 +1708,7 @@ class SunLightSettings:
"xy_color": xy_color,
"hs_color": hs_color,
"sun_position": percent,
+ "force_rgb_color": force_rgb_color,
}
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 59692f1e..9b679a86 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -56,6 +56,7 @@ from custom_components.adaptive_lighting.const import (
ADAPT_BRIGHTNESS_SWITCH,
ADAPT_COLOR_SWITCH,
ATTR_ADAPTIVE_LIGHTING_MANAGER,
+ CONF_ADAPT_UNTIL_SLEEP,
CONF_AUTORESET_CONTROL,
CONF_DETECT_NON_HA_CHANGES,
CONF_INITIAL_TRANSITION,
@@ -64,6 +65,7 @@ from custom_components.adaptive_lighting.const import (
CONF_MIN_COLOR_TEMP,
CONF_PREFER_RGB_COLOR,
CONF_SEPARATE_TURN_ON_COMMANDS,
+ CONF_SLEEP_RGB_OR_COLOR_TEMP,
CONF_SUNRISE_OFFSET,
CONF_SUNRISE_TIME,
CONF_SUNSET_TIME,
@@ -74,6 +76,7 @@ from custom_components.adaptive_lighting.const import (
DEFAULT_NAME,
DEFAULT_SLEEP_BRIGHTNESS,
DEFAULT_SLEEP_COLOR_TEMP,
+ DEFAULT_SLEEP_RGB_COLOR,
DOMAIN,
SERVICE_APPLY,
SERVICE_CHANGE_SWITCH_SETTINGS,
@@ -1430,6 +1433,7 @@ async def test_proactive_adaptation(hass):
{
ATTR_BRIGHTNESS_PCT: 67,
ATTR_COLOR_TEMP_KELVIN: 3448,
+ "force_rgb_color": False,
},
)
@@ -1464,6 +1468,7 @@ async def test_proactive_adaptation_with_separate_commands(hass):
{
ATTR_BRIGHTNESS_PCT: 67,
ATTR_COLOR_TEMP_KELVIN: 3448,
+ "force_rgb_color": False,
},
)
@@ -1615,3 +1620,84 @@ async def test_two_switches_for_single_light(hass):
after_color_temp = attrs[ATTR_COLOR_TEMP_KELVIN]
assert before_brightness != after_brightness
assert before_color_temp != after_color_temp
+
+
+async def test_adapt_until_sleep_and_rgb_colors(hass):
+ """Test setting up the Adaptive Lighting switches with different timezones.
+
+ Also test the (sleep) brightness and color temperature settings.
+ """
+ lat, long, timezone = (32.87336, -117.22743, "US/Pacific")
+ await config_util.async_process_ha_core_config(
+ hass,
+ {"latitude": lat, "longitude": long, "time_zone": timezone},
+ )
+ switch, lights = await setup_lights_and_switch(
+ hass,
+ {
+ CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour),
+ CONF_SUNSET_TIME: datetime.time(SUNSET.hour),
+ CONF_ADAPT_UNTIL_SLEEP: True,
+ CONF_SLEEP_RGB_OR_COLOR_TEMP: "rgb_color",
+ },
+ )
+
+ context = switch.create_context("test") # needs to be passed to update method
+ min_color_temp = switch._sun_light_settings.min_color_temp
+
+ sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC)
+ before_sunset = sunset - datetime.timedelta(hours=1)
+ after_sunset = sunset + datetime.timedelta(hours=1)
+ sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC)
+ before_sunrise = sunrise - datetime.timedelta(hours=1)
+ after_sunrise = sunrise + datetime.timedelta(hours=1)
+
+ async def patch_time_and_update(time):
+ with patch("homeassistant.util.dt.utcnow", return_value=time):
+ await switch._update_attrs_and_maybe_adapt_lights(context=context)
+ await hass.async_block_till_done()
+
+ # At sunset the brightness should be max and color_temp at the smallest value
+ await patch_time_and_update(sunset)
+ assert not switch._settings["force_rgb_color"]
+ assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
+ assert switch._settings["color_temp_kelvin"] == min_color_temp
+
+ # One hour before sunset the brightness should be max and color_temp
+ # not at the smallest value yet.
+ await patch_time_and_update(before_sunset)
+ assert not switch._settings["force_rgb_color"]
+ assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
+ assert switch._settings["color_temp_kelvin"] > min_color_temp
+ assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT]
+
+ # One hour after sunset the brightness should be down
+ await patch_time_and_update(after_sunset)
+ assert switch._settings["force_rgb_color"]
+ assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS
+ assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT]
+
+ # At sunrise the brightness should be max and color_temp at the smallest value
+ await patch_time_and_update(sunrise)
+ assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
+ assert switch._settings["color_temp_kelvin"] == min_color_temp
+ assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT]
+
+ # One hour before sunrise the brightness should smaller than max
+ # and color_temp at the min value.
+ await patch_time_and_update(before_sunrise)
+ assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS
+ assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT]
+
+ # One hour after sunrise the brightness should be up
+ await patch_time_and_update(after_sunrise)
+ assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
+ assert switch._settings["color_temp_kelvin"] > min_color_temp
+ assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT]
+
+ # Turn on sleep mode which make the brightness and color_temp
+ # deterministic regardless of the time
+ await switch.sleep_mode_switch.async_turn_on()
+ await switch._update_attrs_and_maybe_adapt_lights(context=context)
+ assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_SLEEP_BRIGHTNESS
+ assert switch._settings["rgb_color"] == DEFAULT_SLEEP_RGB_COLOR
From bb84684bcee7d25a34b43f9c2b905f3af1040976 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 23 Jul 2023 14:24:22 -0700
Subject: [PATCH 057/549] Style with ruff and logging (#643)
* Formatting with ruff
* more logging
* Require on_only
* Different implementation
* More ruff
* Remove new logging statements
* Remove 'pylint: disable=protected'
* style
* fixes
* ruff
* Switch to ruff
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Fix .github
* Fix
* Add ignores
* fix arg
* fix B905
* Fix D
* Fix more
* Fix more
* order
* use path
* moer path
* Remove unused ignores
* fix tes deps
* fix typo
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.github/update-services.py | 9 +-
.github/update-strings.py | 16 +-
.pre-commit-config.yaml | 16 +-
.ruff.toml | 55 ++--
.../adaptive_lighting/__init__.py | 18 +-
.../adaptive_lighting/_docs_helpers.py | 35 +-
.../adaptive_lighting/adaptation_utils.py | 37 +--
.../adaptive_lighting/config_flow.py | 10 +-
custom_components/adaptive_lighting/const.py | 34 +-
.../adaptive_lighting/hass_utils.py | 30 +-
custom_components/adaptive_lighting/switch.py | 304 +++++++++++-------
test_dependencies.py | 10 +-
12 files changed, 327 insertions(+), 247 deletions(-)
mode change 100755 => 100644 custom_components/adaptive_lighting/__init__.py
diff --git a/.github/update-services.py b/.github/update-services.py
index df4c7b30..ee001beb 100644
--- a/.github/update-services.py
+++ b/.github/update-services.py
@@ -1,5 +1,6 @@
-from pathlib import Path
+"""Creates a services.yaml file with the latest docs."""
import sys
+from pathlib import Path
import yaml
@@ -7,8 +8,8 @@ sys.path.append(str(Path(__file__).parent.parent))
from custom_components.adaptive_lighting import const # noqa: E402
-services_filename = "custom_components/adaptive_lighting/services.yaml"
-with open(services_filename) as f:
+services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml"
+with open(services_filename) as f: # noqa: PTH123
services = yaml.safe_load(f)
for service_name, dct in services.items():
@@ -20,6 +21,6 @@ for service_name, dct in services.items():
comment = "# This file is auto-generated by .github/update-services.py."
-with open(services_filename, "w") as f:
+with services_filename.open("w") as f:
f.write(comment + "\n")
yaml.dump(services, f, sort_keys=False, width=1000, allow_unicode=True)
diff --git a/.github/update-strings.py b/.github/update-strings.py
index aabc7443..8f8f9ef4 100644
--- a/.github/update-strings.py
+++ b/.github/update-strings.py
@@ -1,31 +1,33 @@
+"""Update strings.json and en.json from const.py."""
import json
-from pathlib import Path
import sys
+from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent))
from custom_components.adaptive_lighting import const # noqa: E402
-strings_fname = "custom_components/adaptive_lighting/strings.json"
-en_fname = "custom_components/adaptive_lighting/translations/en.json"
-with open(strings_fname) as f:
+folder = Path("custom_components") / "adaptive_lighting"
+strings_fname = folder / "strings.json"
+en_fname = folder / "translations" / "en.json"
+with strings_fname.open() as f:
strings = json.load(f)
data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES}
strings["options"]["step"]["init"]["data"] = data
-with open(strings_fname, "w") as f:
+with strings_fname.open("w") as f:
json.dump(strings, f, indent=2, ensure_ascii=False)
f.write("\n")
# Sync changes from strings.json to en.json
-with open(en_fname) as f:
+with en_fname.open() as f:
en = json.load(f)
en["config"]["step"]["user"] = strings["config"]["step"]["user"]
en["options"]["step"]["init"]["data"] = data
-with open(en_fname, "w") as f:
+with en_fname.open("w") as f:
json.dump(en, f, indent=2, ensure_ascii=False)
f.write("\n")
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8c35e268..6418f6a3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,20 +7,12 @@ repos:
- id: end-of-file-fixer
- id: mixed-line-ending
args: ["--fix=lf"]
- - repo: https://github.com/pycqa/flake8
- rev: 6.0.0
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.0.279
hooks:
- - id: flake8
+ - id: ruff
+ args: ["--fix"]
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
- - repo: https://github.com/asottile/pyupgrade
- rev: v3.7.0
- hooks:
- - id: pyupgrade
- args: ["--py39-plus"]
- - repo: https://github.com/PyCQA/isort
- rev: 5.12.0
- hooks:
- - id: isort
diff --git a/.ruff.toml b/.ruff.toml
index 260b1883..c1b6b784 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -2,41 +2,30 @@
target-version = "py310"
-select = [
- "B007", # Loop control variable {name} not used within loop body
- "B014", # Exception handler with duplicate exception
- "C", # complexity
- "D", # docstrings
- "E", # pycodestyle
- "F", # pyflakes/autoflake
- "ICN001", # import concentions; {name} should be imported as {asname}
- "PGH004", # Use specific rule codes when using noqa
- "PLC0414", # Useless import alias. Import alias does not rename original package.
- "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass
- "SIM117", # Merge with-statements that use the same scope
- "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys()
- "SIM201", # Use {left} != {right} instead of not {left} == {right}
- "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a}
- "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'.
- "SIM401", # Use get from dict with default instead of an if block
- "T20", # flake8-print
- "TRY004", # Prefer TypeError exception for invalid type
- "RUF006", # Store a reference to the return value of asyncio.create_task
- "UP", # pyupgrade
- "W", # pycodestyle
+select = ["ALL"]
+
+# All the ones without a comment were the ones that are currently violated
+# by the codebase. The plan is to fix them all (when sensible) and then enable them.
+ignore = [
+ "ANN",
+ "ANN101", # Missing type annotation for {name} in method
+ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name}
+ "D401", # First line of docstring should be in imperative mood
+ "E501", # line too long
+ "FBT001", # Boolean positional arg in function definition
+ "FBT002", # Boolean default value in function definition
+ "FIX004", # Line contains HACK, consider resolving the issue
+ "PD901", # df is a bad variable name. Be kinder to your future self.
+ "PERF203",# `try`-`except` within a loop incurs performance overhead
+ "PLR0913", # Too many arguments to function call (N > 5)
+ "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable
+ "S101", # Use of assert detected
+ "SLF001", # Private member accessed
]
-ignore = [
- "D202", # No blank lines allowed after function docstring
- "D203", # 1 blank line required before class docstring
- "D213", # Multi-line docstring summary should start at the second line
- "D404", # First word of the docstring should not be This
- "D406", # Section name should end with a newline
- "D407", # Section name underlining
- "D411", # Missing blank line before section
- "E501", # line too long
- "E731", # do not assign a lambda expression, use a def
-]
+[per-file-ignores]
+"tests/*.py" = ["ALL"]
+".github/*py" = ["INP001"]
[flake8-pytest-style]
fixture-parentheses = false
diff --git a/custom_components/adaptive_lighting/__init__.py b/custom_components/adaptive_lighting/__init__.py
old mode 100755
new mode 100644
index c4187fa7..98c2e94f
--- a/custom_components/adaptive_lighting/__init__.py
+++ b/custom_components/adaptive_lighting/__init__.py
@@ -2,11 +2,11 @@
import logging
from typing import Any
+import homeassistant.helpers.config_validation as cv
+import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_SOURCE
from homeassistant.core import HomeAssistant
-import homeassistant.helpers.config_validation as cv
-import voluptuous as vol
from .const import (
_DOMAIN_SCHEMA,
@@ -35,20 +35,21 @@ CONFIG_SCHEMA = vol.Schema(
)
-async def reload_configuration_yaml(event: dict, hass: HomeAssistant):
+async def reload_configuration_yaml(event: dict, hass: HomeAssistant): # noqa: ARG001
"""Reload configuration.yaml."""
await hass.services.async_call("homeassistant", "check_config", {})
async def async_setup(hass: HomeAssistant, config: dict[str, Any]):
"""Import integration from config."""
-
if DOMAIN in config:
for entry in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
- DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=entry
- )
+ DOMAIN,
+ context={CONF_SOURCE: SOURCE_IMPORT},
+ data=entry,
+ ),
)
return True
@@ -65,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
data[config_entry.entry_id] = {UNDO_UPDATE_LISTENER: undo_listener}
for platform in PLATFORMS:
hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(config_entry, platform)
+ hass.config_entries.async_forward_entry_setup(config_entry, platform),
)
return True
@@ -79,7 +80,8 @@ async def async_update_options(hass, config_entry: ConfigEntry):
async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_forward_entry_unload(
- config_entry, "switch"
+ config_entry,
+ "switch",
)
data = hass.data[DOMAIN]
data[config_entry.entry_id][UNDO_UPDATE_LISTENER]()
diff --git a/custom_components/adaptive_lighting/_docs_helpers.py b/custom_components/adaptive_lighting/_docs_helpers.py
index 40afc235..31225a6c 100644
--- a/custom_components/adaptive_lighting/_docs_helpers.py
+++ b/custom_components/adaptive_lighting/_docs_helpers.py
@@ -1,9 +1,9 @@
from typing import Any
-from homeassistant.helpers import selector
import homeassistant.helpers.config_validation as cv
import pandas as pd
import voluptuous as vol
+from homeassistant.helpers import selector
from .const import (
DOCS,
@@ -23,38 +23,37 @@ def _format_voluptuous_instance(instance):
for validator in instance.validators:
if isinstance(validator, vol.Coerce):
coerce_type = validator.type.__name__
- elif isinstance(validator, (vol.Clamp, vol.Range)):
+ elif isinstance(validator, vol.Clamp | vol.Range):
min_val = validator.min
max_val = validator.max
if min_val is not None and max_val is not None:
return f"`{coerce_type}` {min_val}-{max_val}"
- elif min_val is not None:
+ if min_val is not None:
return f"`{coerce_type} > {min_val}`"
- elif max_val is not None:
+ if max_val is not None:
return f"`{coerce_type} < {max_val}`"
- else:
- return f"`{coerce_type}`"
+ return f"`{coerce_type}`"
-def _type_to_str(type_: Any) -> str:
+def _type_to_str(type_: Any) -> str: # noqa: PLR0911
"""Convert a (voluptuous) type to a string."""
if type_ == cv.entity_ids:
return "list of `entity_id`s"
- elif type_ in (bool, int, float, str):
+ if type_ in (bool, int, float, str):
return f"`{type_.__name__}`"
- elif type_ == cv.boolean:
+ if type_ == cv.boolean:
return "bool"
- elif isinstance(type_, vol.All):
+ if isinstance(type_, vol.All):
return _format_voluptuous_instance(type_)
- elif isinstance(type_, vol.In):
+ if isinstance(type_, vol.In):
return f"one of `{type_.container}`"
- elif isinstance(type_, selector.SelectSelector):
+ if isinstance(type_, selector.SelectSelector):
return f"one of `{type_.config['options']}`"
- elif isinstance(type_, selector.ColorRGBSelector):
+ if isinstance(type_, selector.ColorRGBSelector):
return "RGB color"
- else:
- raise ValueError(f"Unknown type: {type_}")
+ msg = f"Unknown type: {type_}"
+ raise ValueError(msg)
def generate_config_markdown_table():
@@ -85,7 +84,8 @@ def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]:
def _generate_service_markdown_table(
- schema: dict[str, tuple[Any, Any]], alternative_docs: dict[str, str] = None
+ schema: dict[str, tuple[Any, Any]],
+ alternative_docs: dict[str, str] | None = None,
):
schema = _schema_to_dict(schema)
rows = []
@@ -112,5 +112,6 @@ def generate_apply_markdown_table():
def generate_set_manual_control_markdown_table():
return _generate_service_markdown_table(
- SET_MANUAL_CONTROL_SCHEMA, DOCS_MANUAL_CONTROL
+ SET_MANUAL_CONTROL_SCHEMA,
+ DOCS_MANUAL_CONTROL,
)
diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py
index 374defd1..99d1eea5 100644
--- a/custom_components/adaptive_lighting/adaptation_utils.py
+++ b/custom_components/adaptive_lighting/adaptation_utils.py
@@ -1,7 +1,7 @@
"""Utility functions for adaptation commands."""
+import logging
from collections.abc import AsyncGenerator
from dataclasses import dataclass
-import logging
from typing import Any, Literal
from homeassistant.components.light import (
@@ -40,10 +40,10 @@ ServiceData = dict[str, Any]
def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]:
- """Splits the service data by the adapted attributes, i.e., into separate data
- items for brightness and color.
- """
+ """Splits the service data by the adapted attributes.
+ i.e., into separate data items for brightness and color.
+ """
common_attrs = {ATTR_ENTITY_ID}
common_data = {k: service_data[k] for k in common_attrs if k in service_data}
@@ -70,13 +70,14 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]:
def _remove_redundant_attributes(
- service_data: ServiceData, state: State
+ service_data: ServiceData,
+ state: State,
) -> ServiceData:
"""Filter service data by removing attributes that already equal the given state.
Removes all attributes from service call data whose values are already present
- in the target entity's state."""
-
+ in the target entity's state.
+ """
return {
k: v
for k, v in service_data.items()
@@ -88,7 +89,8 @@ def _has_relevant_service_data_attributes(service_data: ServiceData) -> bool:
"""Determines whether the service data justifies an adaptation service call.
A service call is not justified for data which does not contain any entries that
- change relevant attributes of an adapting entity, e.g., brightness or color."""
+ change relevant attributes of an adapting entity, e.g., brightness or color.
+ """
common_attrs = {ATTR_ENTITY_ID, ATTR_TRANSITION}
return any(attr not in common_attrs for attr in service_data)
@@ -108,15 +110,15 @@ async def _create_service_call_data_iterator(
at the time when the service data is read instead of up front. This gives greater
flexibility because entity states can change while the items are iterated.
"""
-
for service_data in service_datas:
if filter_by_state and (entity_id := service_data.get(ATTR_ENTITY_ID)):
current_entity_state = hass.states.get(entity_id)
# Filter data to remove attributes that equal the current state
if current_entity_state is not None:
- service_data = _remove_redundant_attributes(
- service_data, current_entity_state
+ service_data = _remove_redundant_attributes( # noqa: PLW2901
+ service_data,
+ state=current_entity_state,
)
# Emit service data if it still contains relevant attributes (else try next)
@@ -143,7 +145,7 @@ class AdaptationData:
return await anext(self.service_call_datas, None)
-class NoColorOrBrightnessInServiceData(Exception):
+class NoColorOrBrightnessInServiceDataError(Exception):
"""Exception raised when no color or brightness attributes are found in service data."""
@@ -160,7 +162,7 @@ def _identify_lighting_type(
if has_color:
return "color"
msg = f"Invalid service_data, no brightness or color attributes found: {service_data=}"
- raise NoColorOrBrightnessInServiceData(msg)
+ raise NoColorOrBrightnessInServiceDataError(msg)
def prepare_adaptation_data(
@@ -179,10 +181,7 @@ def prepare_adaptation_data(
entity_id,
service_data,
)
- if split:
- service_datas = _split_service_call_data(service_data)
- else:
- service_datas = [service_data]
+ service_datas = _split_service_call_data(service_data) if split else [service_data]
service_datas_length = len(service_datas)
@@ -193,7 +192,9 @@ def prepare_adaptation_data(
sleep_time = split_delay
service_data_iterator = _create_service_call_data_iterator(
- hass, service_datas, filter_by_state
+ hass,
+ service_datas,
+ filter_by_state,
)
lighting_type = _identify_lighting_type(service_data)
diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py
index 10ba5d86..170c8503 100644
--- a/custom_components/adaptive_lighting/config_flow.py
+++ b/custom_components/adaptive_lighting/config_flow.py
@@ -1,11 +1,11 @@
"""Config flow for Adaptive Lighting integration."""
import logging
+import homeassistant.helpers.config_validation as cv
+import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
-import voluptuous as vol
from .const import ( # pylint: disable=unused-import
CONF_LIGHTS,
@@ -75,7 +75,7 @@ def validate_options(user_input, errors):
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow for Adaptive Lighting."""
- def __init__(self, config_entry: config_entries.ConfigEntry):
+ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
@@ -114,5 +114,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
options_schema[key] = value
return self.async_show_form(
- step_id="init", data_schema=vol.Schema(options_schema), errors=errors
+ step_id="init",
+ data_schema=vol.Schema(options_schema),
+ errors=errors,
)
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index 15ce097b..64b96583 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -1,10 +1,10 @@
"""Constants for the Adaptive Lighting integration."""
+import homeassistant.helpers.config_validation as cv
+import voluptuous as vol
from homeassistant.components.light import VALID_TRANSITION
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.helpers import selector
-import homeassistant.helpers.config_validation as cv
-import voluptuous as vol
ICON_MAIN = "mdi:theme-light-dark"
ICON_BRIGHTNESS = "mdi:brightness-4"
@@ -49,9 +49,9 @@ DOCS[CONF_INITIAL_TRANSITION] = (
)
CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1
-DOCS[CONF_SLEEP_TRANSITION] = (
- 'Duration of transition when "sleep mode" is toggled ' "in seconds. 😴"
-)
+DOCS[
+ CONF_SLEEP_TRANSITION
+] = 'Duration of transition when "sleep mode" is toggled in seconds. 😴'
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90
DOCS[CONF_INTERVAL] = "Frequency to adapt the lights, in seconds. 🔄"
@@ -99,22 +99,22 @@ DOCS[CONF_SLEEP_COLOR_TEMP] = (
)
CONF_SLEEP_RGB_COLOR, DEFAULT_SLEEP_RGB_COLOR = "sleep_rgb_color", [255, 56, 0]
-DOCS[CONF_SLEEP_RGB_COLOR] = (
- "RGB color in sleep mode (used when " '`sleep_rgb_or_color_temp` is "rgb_color"). 🌈'
-)
+DOCS[
+ CONF_SLEEP_RGB_COLOR
+] = 'RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈'
CONF_SLEEP_RGB_OR_COLOR_TEMP, DEFAULT_SLEEP_RGB_OR_COLOR_TEMP = (
"sleep_rgb_or_color_temp",
"color_temp",
)
-DOCS[CONF_SLEEP_RGB_OR_COLOR_TEMP] = (
- 'Use either `"rgb_color"` or `"color_temp"` ' "in sleep mode. 🌙"
-)
+DOCS[
+ CONF_SLEEP_RGB_OR_COLOR_TEMP
+] = 'Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙'
CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET = "sunrise_offset", 0
-DOCS[CONF_SUNRISE_OFFSET] = (
- "Adjust sunrise time with a positive or negative offset " "in seconds. ⏰"
-)
+DOCS[
+ CONF_SUNRISE_OFFSET
+] = "Adjust sunrise time with a positive or negative offset in seconds. ⏰"
CONF_SUNRISE_TIME = "sunrise_time"
DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅"
@@ -333,7 +333,7 @@ _DOMAIN_SCHEMA = vol.Schema(
{
vol.Optional(key, default=replace_none_str(default, vol.UNDEFINED)): validation
for key, default, validation in _yaml_validation_tuples
- }
+ },
)
@@ -351,7 +351,7 @@ def apply_service_schema(initial_transition: int = 1):
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,
- }
+ },
)
@@ -360,5 +360,5 @@ SET_MANUAL_CONTROL_SCHEMA = vol.Schema(
vol.Optional(CONF_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_LIGHTS, default=[]): cv.entity_ids,
vol.Optional(CONF_MANUAL_CONTROL, default=True): cv.boolean,
- }
+ },
)
diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py
index 5a195bcc..3f08b3fc 100644
--- a/custom_components/adaptive_lighting/hass_utils.py
+++ b/custom_components/adaptive_lighting/hass_utils.py
@@ -1,6 +1,5 @@
"""Utility functions for HA core."""
-from collections.abc import Awaitable
-from typing import Callable
+from collections.abc import Awaitable, Callable
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.util.read_only_dict import ReadOnlyDict
@@ -17,7 +16,8 @@ def setup_service_call_interceptor(
"""Inject a function into a registered service call to preprocess service data.
The injected interceptor function receives the service call and a writeable data dictionary
- (the data of the service call is read-only) before the service call is executed."""
+ (the data of the service call is read-only) before the service call is executed.
+ """
try:
# HACK: Access protected attribute of HA service registry.
# This is necessary to replace a registered service handler with our
@@ -26,15 +26,15 @@ def setup_service_call_interceptor(
hass.services._services # pylint: disable=protected-access
)
except AttributeError as error:
- raise RuntimeError(
- "Intercept failed because registered services are no longer accessible "
- "(internal API may have changed)"
- ) from error
+ msg = (
+ "Intercept failed because registered services are no longer"
+ " accessible (internal API may have changed)"
+ )
+ raise RuntimeError(msg) from error
if domain not in registered_services or service not in registered_services[domain]:
- raise RuntimeError(
- f"Intercept failed because service {domain}.{service} is not registered"
- )
+ msg = f"Intercept failed because service {domain}.{service} is not registered"
+ raise RuntimeError(msg)
existing_service = registered_services[domain][service]
@@ -52,13 +52,19 @@ def setup_service_call_interceptor(
await existing_service.job.target(call)
hass.services.async_register(
- domain, service, service_func_proxy, existing_service.schema
+ domain,
+ service,
+ service_func_proxy,
+ existing_service.schema,
)
def remove():
# Remove the interceptor by reinstalling the original service handler
hass.services.async_register(
- domain, service, existing_service.job.target, existing_service.schema
+ domain,
+ service,
+ existing_service.job.target,
+ existing_service.schema,
)
return remove
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 58fa3eaa..d42cccae 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -4,17 +4,19 @@ from __future__ import annotations
import asyncio
import base64
import bisect
-from collections.abc import Callable, Coroutine, Iterable
-from copy import deepcopy
-from dataclasses import dataclass
import datetime
-from datetime import timedelta
import functools
import logging
import math
-from typing import Any, Literal
+from copy import deepcopy
+from dataclasses import dataclass
+from datetime import timedelta
+from typing import TYPE_CHECKING, Any, Literal
-import astral
+import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt_util
+import ulid_transform
+import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
@@ -29,8 +31,6 @@ from homeassistant.components.light import (
COLOR_MODE_RGBW,
COLOR_MODE_RGBWW,
COLOR_MODE_XY,
-)
-from homeassistant.components.light import (
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
@@ -41,7 +41,6 @@ from homeassistant.components.light import (
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.switch import SwitchEntity
-from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_AREA_ID,
ATTR_DOMAIN,
@@ -71,7 +70,6 @@ from homeassistant.core import (
callback,
)
from homeassistant.helpers import entity_platform, entity_registry
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import (
async_track_state_change_event,
async_track_time_interval,
@@ -86,9 +84,6 @@ from homeassistant.util.color import (
color_xy_to_hs,
color_xy_to_RGB,
)
-import homeassistant.util.dt as dt_util
-import ulid_transform
-import voluptuous as vol
from .adaptation_utils import (
BRIGHTNESS_ATTRS,
@@ -156,6 +151,13 @@ from .const import (
)
from .hass_utils import setup_service_call_interceptor
+if TYPE_CHECKING:
+ from collections.abc import Callable, Coroutine, Iterable
+
+ import astral
+ from homeassistant.config_entries import ConfigEntry
+ from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
_SUPPORT_OPTS = {
"brightness": SUPPORT_BRIGHTNESS,
"color_temp": SUPPORT_COLOR_TEMP,
@@ -209,17 +211,17 @@ def _int_to_base36(num: int) -> str:
>>> print(base36_num)
'2N9'
"""
- ALPHANUMERIC_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
if num == 0:
- return ALPHANUMERIC_CHARS[0]
+ return alphanumeric_chars[0]
base36_str = ""
- base = len(ALPHANUMERIC_CHARS)
+ base = len(alphanumeric_chars)
while num:
num, remainder = divmod(num, base)
- base36_str = ALPHANUMERIC_CHARS[remainder] + base36_str
+ base36_str = alphanumeric_chars[remainder] + base36_str
return base36_str
@@ -236,7 +238,10 @@ def _remove_vowels(input_str: str, length: int = 4) -> str:
def create_context(
- name: str, which: str, index: int, parent: Context | None = None
+ name: str,
+ which: str,
+ index: int,
+ parent: Context | None = None,
) -> Context:
"""Create a context that can identify this integration."""
# Use a hash for the name because otherwise the context might become
@@ -255,6 +260,7 @@ def create_context(
def is_our_context_id(context_id: str | None) -> bool:
+ """Check whether this integration created 'context_id'."""
if context_id is None:
return False
return f":{_DOMAIN_SHORT}:" in context_id
@@ -268,7 +274,8 @@ def is_our_context(context: Context | None) -> bool:
def _switches_with_lights(
- hass: HomeAssistant, lights: list[str]
+ hass: HomeAssistant,
+ lights: list[str],
) -> list[AdaptiveSwitch]:
"""Get all switches that control at least one of the lights passed."""
config_entries = hass.config_entries.async_entries(DOMAIN)
@@ -299,48 +306,52 @@ def _switch_with_lights(
switches = _switches_with_lights(hass, lights)
if len(switches) == 1:
return switches[0]
- elif len(switches) > 1:
+ if len(switches) > 1:
on_switches = [s for s in switches if s.is_on]
if len(on_switches) == 1:
# Of the multiple switches, only one is on
return on_switches[0]
- raise NoSwitchFoundError(
+ msg = (
f"_switch_with_lights: Light(s) {lights} found in multiple switch configs"
f" ({[s.entity_id for s in switches]}). You must pass a switch under"
- f" 'entity_id'."
- )
- else:
- raise NoSwitchFoundError(
- f"_switch_with_lights: Light(s) {lights} not found in any switch's"
- f" configuration. You must either include the light(s) that is/are"
- f" in the integration config, or pass a switch under 'entity_id'."
+ " 'entity_id'."
)
+ raise NoSwitchFoundError(msg)
+ msg = (
+ f"_switch_with_lights: Light(s) {lights} not found in any switch's"
+ " configuration. You must either include the light(s) that is/are"
+ " in the integration config, or pass a switch under 'entity_id'."
+ )
+ raise NoSwitchFoundError(msg)
# For documentation on this function, see integration_entities() from HomeAssistant Core:
# https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109
def _switches_from_service_call(
- hass: HomeAssistant, service_call: ServiceCall
+ hass: HomeAssistant,
+ service_call: ServiceCall,
) -> list[AdaptiveSwitch]:
data = service_call.data
lights = data[CONF_LIGHTS]
switch_entity_ids: list[str] | None = data.get("entity_id")
if not lights and not switch_entity_ids:
- raise ValueError(
+ msg = (
"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 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."
)
+ raise ValueError(msg)
if switch_entity_ids is not None:
if len(switch_entity_ids) > 1 and lights:
- raise ValueError(
- f"adaptive-lighting: Cannot pass multiple switches with lights argument."
+ msg = (
+ "adaptive-lighting: Cannot pass multiple switches with lights argument."
f" Invalid service data received: {service_call.data}"
)
+ raise ValueError(msg)
switches = []
ent_reg = entity_registry.async_get(hass)
for entity_id in switch_entity_ids:
@@ -353,14 +364,16 @@ def _switches_from_service_call(
switch = _switch_with_lights(hass, lights)
return [switch]
- raise ValueError(
- f"adaptive-lighting: Incorrect data provided in service call."
+ msg = (
+ "adaptive-lighting: Incorrect data provided in service call."
f" Entities not found in the integration. Service data: {service_call.data}"
)
+ raise ValueError(msg)
async def handle_change_switch_settings(
- switch: AdaptiveSwitch, service_call: ServiceCall
+ switch: AdaptiveSwitch,
+ service_call: ServiceCall,
) -> None:
"""Allows HASS to change config values via a service call."""
data = service_call.data
@@ -396,7 +409,10 @@ async def handle_change_switch_settings(
@callback
def _fire_manual_control_event(
- switch: AdaptiveSwitch, light: str, context: Context, is_async=True
+ switch: AdaptiveSwitch,
+ light: str,
+ context: Context,
+ is_async: bool = True,
):
"""Fire an event that 'light' is marked as manual_control."""
hass = switch.hass
@@ -414,8 +430,10 @@ def _fire_manual_control_event(
)
-async def async_setup_entry(
- hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: bool
+async def async_setup_entry( # noqa: PLR0915
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
):
"""Set up the AdaptiveLighting switch."""
data = hass.data[DOMAIN]
@@ -423,17 +441,30 @@ async def async_setup_entry(
if ATTR_ADAPTIVE_LIGHTING_MANAGER not in data:
data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = AdaptiveLightingManager(
- hass, config_entry
+ hass,
+ config_entry,
)
manager: AdaptiveLightingManager = data[ATTR_ADAPTIVE_LIGHTING_MANAGER]
sleep_mode_switch = SimpleSwitch(
- "Sleep Mode", False, hass, config_entry, ICON_SLEEP
+ which="Sleep Mode",
+ initial_state=False,
+ hass=hass,
+ config_entry=config_entry,
+ icon=ICON_SLEEP,
)
adapt_color_switch = SimpleSwitch(
- "Adapt Color", True, hass, config_entry, ICON_COLOR_TEMP
+ which="Adapt Color",
+ initial_state=True,
+ hass=hass,
+ config_entry=config_entry,
+ icon=ICON_COLOR_TEMP,
)
adapt_brightness_switch = SimpleSwitch(
- "Adapt Brightness", True, hass, config_entry, ICON_BRIGHTNESS
+ which="Adapt Brightness",
+ initial_state=True,
+ hass=hass,
+ config_entry=config_entry,
+ icon=ICON_BRIGHTNESS,
)
switch = AdaptiveSwitch(
hass,
@@ -482,7 +513,8 @@ async def async_setup_entry(
data[ATTR_ADAPT_COLOR],
data[CONF_PREFER_RGB_COLOR],
context=switch.create_context(
- "service", parent=service_call.context
+ "service",
+ parent=service_call.context,
),
)
@@ -513,7 +545,8 @@ async def async_setup_entry(
transition=switch.initial_transition,
force=True,
context=switch.create_context(
- "service", parent=service_call.context
+ "service",
+ parent=service_call.context,
),
)
@@ -637,7 +670,8 @@ def _supported_features(hass: HomeAssistant, light: str) -> set[str]:
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:
"""Distance between colors in RGB space (redmean metric).
@@ -648,7 +682,9 @@ def color_difference_redmean(
- https://www.compuphase.com/cmetric.htm
"""
r_hat = (rgb1[0] + rgb2[0]) / 2
- delta_r, delta_g, delta_b = ((col1 - col2) for col1, col2 in zip(rgb1, rgb2))
+ delta_r, delta_g, delta_b = (
+ (col1 - col2) for col1, col2 in zip(rgb1, rgb2, strict=True)
+ )
red_term = (2 + r_hat / 256) * delta_r**2
green_term = 4 * delta_g**2
blue_term = (2 + (255 - r_hat) / 256) * delta_b**2
@@ -700,7 +736,8 @@ def _attributes_have_changed(
) -> bool:
if adapt_color:
old_attributes, new_attributes = _add_missing_attributes(
- old_attributes, new_attributes
+ old_attributes,
+ new_attributes,
)
if (
@@ -771,7 +808,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
sleep_mode_switch: SimpleSwitch,
adapt_color_switch: SimpleSwitch,
adapt_brightness_switch: SimpleSwitch,
- ):
+ ) -> None:
"""Initialize the Adaptive Lighting switch."""
# Set attributes that can't be modified during runtime
self.hass = hass
@@ -920,7 +957,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
await self._setup_listeners()
else:
self.hass.bus.async_listen_once(
- EVENT_HOMEASSISTANT_STARTED, self._setup_listeners
+ EVENT_HOMEASSISTANT_STARTED,
+ self._setup_listeners,
)
last_state = await self.async_get_last_state()
is_new_entry = last_state is None # newly added to HA
@@ -938,7 +976,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
all_lights = _expand_light_groups(self.hass, self.lights)
self.manager.lights.update(all_lights)
self.manager.set_auto_reset_manual_control_times(
- all_lights, self._auto_reset_manual_control_time
+ all_lights,
+ self._auto_reset_manual_control_time,
)
self.lights = list(all_lights)
@@ -963,7 +1002,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if self.lights:
self._expand_light_groups()
remove_state = async_track_state_change_event(
- self.hass, entity_ids=self.lights, action=self._light_event_action
+ self.hass,
+ entity_ids=self.lights,
+ action=self._light_event_action,
)
self.remove_listeners.append(remove_state)
@@ -1029,7 +1070,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
return extra_state_attributes
def create_context(
- self, which: str = "default", parent: Context | None = None
+ self,
+ which: str = "default",
+ parent: Context | None = None,
) -> Context:
"""Create a context that identifies this Adaptive Lighting instance."""
context = create_context(self._name, which, self._context_cnt, parent=parent)
@@ -1037,11 +1080,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
return context
async def async_turn_on( # pylint: disable=arguments-differ
- self, adapt_lights: bool = True
+ self,
+ adapt_lights: bool = True,
) -> None:
"""Turn on adaptive lighting."""
_LOGGER.debug(
- "%s: Called 'async_turn_on', current state is '%s'", self._name, self._state
+ "%s: Called 'async_turn_on', current state is '%s'",
+ self._name,
+ self._state,
)
if self.is_on:
return
@@ -1055,7 +1101,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context=self.create_context("turn_on"),
)
- async def async_turn_off(self, **kwargs) -> None:
+ async def async_turn_off(self, **kwargs) -> None: # noqa: ARG002
"""Turn off adaptive lighting."""
if not self.is_on:
return
@@ -1063,7 +1109,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._remove_listeners()
self.manager.reset(*self.lights)
- async def _async_update_at_interval_action(self, now=None) -> None:
+ async def _async_update_at_interval_action(self, now=None) -> None: # noqa: ARG002
await self._update_attrs_and_maybe_adapt_lights(
transition=self._transition,
force=False,
@@ -1079,6 +1125,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
prefer_rgb_color: bool | None = None,
context: Context | None = None,
) -> AdaptationData | None:
+ """Prepare `AdaptationData` for adapting a light."""
if transition is None:
transition = self._transition
if adapt_brightness is None:
@@ -1099,7 +1146,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# The switch might be off and not have _settings set.
self._settings = self._sun_light_settings.get_settings(
- self.sleep_mode_switch.is_on, transition
+ self.sleep_mode_switch.is_on,
+ transition,
)
# Build service data.
@@ -1151,7 +1199,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
filter_by_state=self._skip_redundant_commands,
)
- async def _adapt_light( # noqa: C901
+ async def _adapt_light(
self,
light: str,
transition: int | None = None,
@@ -1167,7 +1215,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
if self.manager.is_proactively_adapting(context.parent_id):
# Skip if adaptation was already executed by the service call interceptor
_LOGGER.debug(
- "%s: Skipping reactive adaptation of %s", self._name, context.parent_id
+ "%s: Skipping reactive adaptation of %s",
+ self._name,
+ context.parent_id,
)
return
@@ -1180,13 +1230,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context,
)
if data is None:
- return None # nothing to adapt
+ return # nothing to adapt
await self.execute_cancellable_adaptation_calls(data)
async def _execute_adaptation_calls(self, data: AdaptationData):
"""Executes a sequence of adaptation service calls for the given service datas."""
-
for index in range(data.max_length):
is_first_call = index == 0
@@ -1250,7 +1299,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
data,
)
- async def _update_attrs_and_maybe_adapt_lights(
+ async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912
self,
lights: list[str] | None = None,
transition: int | None = None,
@@ -1270,8 +1319,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
assert self.is_on
self._settings.update(
self._sun_light_settings.get_settings(
- self.sleep_mode_switch.is_on, transition
- )
+ self.sleep_mode_switch.is_on,
+ transition,
+ ),
)
self.async_write_ha_state()
@@ -1352,7 +1402,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
_LOGGER.debug("%s: Ignoring sleep event %s", self._name, event)
return
_LOGGER.debug(
- "%s: _sleep_mode_switch_state_event_action, event: '%s'", self._name, event
+ "%s: _sleep_mode_switch_state_event_action, event: '%s'",
+ self._name,
+ event,
)
# Reset the manually controlled status when the "sleep mode" changes
self.manager.reset(*self.lights)
@@ -1380,7 +1432,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
if event.context.parent_id and not self.manager.is_proactively_adapting(
- event.context.id
+ event.context.id,
):
self.manager.reset(entity_id, reset_manual_control=False)
@@ -1395,7 +1447,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
):
# Stop if a rapid 'off' → 'on' → 'off' happens.
_LOGGER.debug(
- "%s: Cancelling adjusting lights for %s", self._name, entity_id
+ "%s: Cancelling adjusting lights for %s",
+ self._name,
+ entity_id,
)
return
@@ -1441,7 +1495,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
hass: HomeAssistant,
config_entry: ConfigEntry,
icon: str,
- ):
+ ) -> None:
"""Initialize the Adaptive Lighting switch."""
self.hass = hass
data = validate(config_entry)
@@ -1484,19 +1538,21 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
else:
await self.async_turn_off()
- async def async_turn_on(self, **kwargs) -> None:
+ async def async_turn_on(self, **kwargs) -> None: # noqa: ARG002
"""Turn on adaptive lighting sleep mode."""
_LOGGER.debug("%s: Turning on", self._name)
self._state = True
- async def async_turn_off(self, **kwargs) -> None:
+ async def async_turn_off(self, **kwargs) -> None: # noqa: ARG002
"""Turn off adaptive lighting sleep mode."""
_LOGGER.debug("%s: Turning off", self._name)
self._state = False
def lerp_color(
- rgb1: tuple[int, int, int], rgb2: tuple[int, int, int], t: float
+ rgb1: tuple[int, int, int],
+ rgb2: tuple[int, int, int],
+ t: float,
) -> tuple[int, int, int]:
"""Linearly interpolate between two RGB colors."""
return (
@@ -1536,13 +1592,13 @@ class SunLightSettings:
def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime:
time = getattr(self, f"{key}_time")
date_time = datetime.datetime.combine(date, time)
- utc_time = date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(
- dt_util.UTC
+ return date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(
+ dt_util.UTC,
)
- return utc_time
def calculate_noon_and_midnight(
- sunset: datetime.datetime, sunrise: datetime.datetime
+ sunset: datetime.datetime,
+ sunrise: datetime.datetime,
) -> tuple[datetime.datetime, datetime.datetime]:
middle = abs(sunset - sunrise) / 2
if sunset > sunrise:
@@ -1597,7 +1653,7 @@ class SunLightSettings:
]
# Check whether order is correct
events = sorted(events, key=lambda x: x[1])
- events_names, _ = zip(*events)
+ events_names, _ = zip(*events, strict=True)
if events_names not in _ALLOWED_ORDERS:
msg = (
f"{self.name}: The sun events {events_names} are not in the expected"
@@ -1635,8 +1691,7 @@ class SunLightSettings:
else (next_ts, prev_ts)
)
k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1
- percentage = (0 - k) * ((target_ts - h) / (h - x)) ** 2 + k
- return percentage
+ return (0 - k) * ((target_ts - h) / (h - x)) ** 2 + k
def calc_brightness_pct(self, percent: float, is_sleep: bool) -> float:
"""Calculate the brightness in %."""
@@ -1660,9 +1715,12 @@ class SunLightSettings:
delta = abs(self.min_color_temp - self.sleep_color_temp)
ct = (delta * abs(1 + percent)) + self.sleep_color_temp
return 5 * round(ct / 5) # round to nearest 5
+ return None
def get_settings(
- self, is_sleep, transition
+ self,
+ is_sleep,
+ transition,
) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]:
"""Get all light settings.
@@ -1715,7 +1773,7 @@ class SunLightSettings:
class AdaptiveLightingManager:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
- def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
+ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the AdaptiveLightingManager that is shared among all switches."""
self.hass = hass
data = validate(config_entry)
@@ -1759,7 +1817,8 @@ class AdaptiveLightingManager:
self._proactively_adapting_contexts: dict[str, str] = {}
is_proactive_adaptation_enabled = data.get(
- INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION, True
+ INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION,
+ True,
)
if is_proactive_adaptation_enabled:
@@ -1770,7 +1829,7 @@ class AdaptiveLightingManager:
LIGHT_DOMAIN,
SERVICE_TURN_ON,
self._service_interceptor_turn_on_handler,
- )
+ ),
)
self.listener_removers.append(
@@ -1779,7 +1838,7 @@ class AdaptiveLightingManager:
LIGHT_DOMAIN,
SERVICE_TOGGLE,
self._service_interceptor_turn_on_handler,
- )
+ ),
)
_LOGGER.debug("Proactive adaptation enabled")
@@ -1796,20 +1855,21 @@ class AdaptiveLightingManager:
remove()
def set_proactively_adapting(self, context_id: str, entity_id: str) -> None:
- """Declare the adaptation with the given context ID as proactively adapting,
- and associate it to an entity ID."""
+ """Declare the adaptation with context_id as proactively adapting,
+ and associate it to an entity_id.
+ """ # noqa: D205
self._proactively_adapting_contexts[context_id] = entity_id
def is_proactively_adapting(self, context_id: str) -> bool:
- """Determine whether an adaptation with the given context ID is proactive."""
+ """Determine whether an adaptation with the given context_id is proactive."""
is_proactively_adapting_context = (
context_id in self._proactively_adapting_contexts
)
_LOGGER.debug(
- "is_proactively_adapting_context %s %s",
- context_id,
+ "is_proactively_adapting_context='%s', context_id='%s'",
is_proactively_adapting_context,
+ context_id,
)
return is_proactively_adapting_context
@@ -1817,16 +1877,19 @@ class AdaptiveLightingManager:
def clear_proactively_adapting(self, entity_id: str) -> None:
"""Clear all context IDs associated with the given entity ID.
- Call this method to clear past context IDs and avoid a memory leak."""
+ Call this method to clear past context IDs and avoid a memory leak.
+ """
+ # First get the keys to avoid modifying the dict while iterating it
keys = [
k for k, v in self._proactively_adapting_contexts.items() if v == entity_id
]
-
for key in keys:
self._proactively_adapting_contexts.pop(key)
- async def _service_interceptor_turn_on_handler(
- self, call: ServiceCall, data: ServiceData
+ async def _service_interceptor_turn_on_handler( # noqa: PLR0911
+ self,
+ call: ServiceCall,
+ data: ServiceData,
):
# Don't adapt our own service calls
if is_our_context(call.context):
@@ -1872,14 +1935,17 @@ class AdaptiveLightingManager:
return
_LOGGER.debug(
- "Intercepted TURN_ON call with data %s (%s)", data, call.context.id
+ "Intercepted TURN_ON call with data %s (%s)",
+ data,
+ call.context.id,
)
self.reset(entity_id, reset_manual_control=False)
self.clear_proactively_adapting(entity_id)
transition = data[CONF_PARAMS].get(
- ATTR_TRANSITION, adaptive_switch.initial_transition
+ ATTR_TRANSITION,
+ adaptive_switch.initial_transition,
)
adaptation_data = await adaptive_switch.prepare_adaptation_data(
@@ -1913,8 +1979,8 @@ class AdaptiveLightingManager:
self.set_proactively_adapting(call.context.id, entity_id)
self.set_proactively_adapting(adaptation_data.context.id, entity_id)
adaptation_data.initial_sleep = True
- asyncio.create_task( # Don't await to avoid blocking the service call
- adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data)
+ _ = asyncio.create_task( # Don't await to avoid blocking the service call
+ adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data),
)
def _handle_timer(
@@ -1946,11 +2012,14 @@ class AdaptiveLightingManager:
last_transition = last_service_data.get(ATTR_TRANSITION)
if not last_transition:
_LOGGER.debug(
- "No transition in last adapt for light %s, continuing...", light
+ "No transition in last adapt for light %s, continuing...",
+ light,
)
return
_LOGGER.debug(
- "Start transition timer of %s seconds for light %s", last_transition, light
+ "Start transition timer of %s seconds for light %s",
+ last_transition,
+ light,
)
async def reset():
@@ -2008,7 +2077,9 @@ class AdaptiveLightingManager:
self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset)
def cancel_ongoing_adaptation_calls(
- self, light_id: str, which: Literal["color", "brightness", "both"] = "both"
+ self,
+ light_id: str,
+ which: Literal["color", "brightness", "both"] = "both",
):
"""Cancel ongoing adaptation service calls for a specific light entity."""
brightness_task = self.adaptation_tasks_brightness.get(light_id)
@@ -2054,15 +2125,21 @@ class AdaptiveLightingManager:
area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID])
for area_id in area_ids:
area_entity_ids = area_entities(self.hass, area_id)
- for entity_id in area_entity_ids:
- if entity_id.startswith(LIGHT_DOMAIN):
- entity_ids.append(entity_id)
+ eids = [
+ entity_id
+ for entity_id in area_entity_ids
+ if entity_id.startswith(LIGHT_DOMAIN)
+ ]
+ entity_ids.extend(eids)
_LOGGER.debug(
- "Found entity_ids '%s' for area_id '%s'", entity_ids, area_id
+ "Found entity_ids '%s' for area_id '%s'",
+ entity_ids,
+ area_id,
)
else:
_LOGGER.debug(
- "No entity_ids or area_ids found in service_data: %s", service_data
+ "No entity_ids or area_ids found in service_data: %s",
+ service_data,
)
return entity_ids
@@ -2219,7 +2296,7 @@ class AdaptiveLightingManager:
"""
last_service_data = self.last_service_data.get(light)
if last_service_data is None:
- return
+ return None
compare_to = functools.partial(
_attributes_have_changed,
light=light,
@@ -2268,8 +2345,11 @@ class AdaptiveLightingManager:
)
return False
- async def maybe_cancel_adjusting(
- self, entity_id: str, off_to_on_event: Event, on_to_off_event: Event | None
+ async def maybe_cancel_adjusting( # noqa: PLR0911, PLR0912
+ self,
+ entity_id: str,
+ off_to_on_event: Event,
+ on_to_off_event: Event | None,
) -> bool:
"""Cancel the adjusting of a light if it has just been turned off.
@@ -2368,7 +2448,7 @@ class AdaptiveLightingManager:
class _AsyncSingleShotTimer:
- def __init__(self, delay, callback):
+ def __init__(self, delay, callback) -> None:
"""Initialize the timer."""
self.delay = delay
self.callback = callback
diff --git a/test_dependencies.py b/test_dependencies.py
index 2cf8dfb0..f8fbf5b9 100644
--- a/test_dependencies.py
+++ b/test_dependencies.py
@@ -1,13 +1,17 @@
+"""Extracts the dependencies of the components required for testing."""
from collections import defaultdict
+from pathlib import Path
deps = defaultdict(list)
components, packages = [], []
-with open("core/requirements_test_all.txt") as f:
+requirements = Path("core") / "requirements_test_all.txt"
+
+with requirements.open() as f:
lines = f.readlines()
for line in lines:
- line = line.strip()
+ line = line.strip() # noqa: PLW2901
if line.startswith("# homeassistant."):
if components and packages:
@@ -34,4 +38,4 @@ required = [
]
to_install = [package for r in required for package in deps[r]]
-print(" ".join(to_install))
+print(" ".join(to_install)) # noqa: T201
From 7650a1b3e633cc085af8b18d8a2c78df50f2d911 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 23 Jul 2023 14:44:17 -0700
Subject: [PATCH 058/549] Bump to 1.17.0 in manifest.json (#657)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index fcf644cd..c3a9c50a 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.16.3"
+ "version": "1.17.0"
}
From f9b6753e6d7a6a48aa6ad3a34ec5d66ab871f8ce Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 25 Jul 2023 10:46:34 -0700
Subject: [PATCH 059/549] Ensure that interpolation is in [0, 1], fixes #624
(#660)
* Ensure that interpolation is in [0, 1], fixes #624
* Bump to 1.17.1
* Add assert
* Interpolate via HSV space
* Add test
---
.../adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 28 ++++++++++++++-----
tests/test_switch.py | 17 +++++++++++
3 files changed, 39 insertions(+), 8 deletions(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index c3a9c50a..747fa2dd 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.17.0"
+ "version": "1.17.1"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index d42cccae..ce9f6922 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
import base64
import bisect
+import colorsys
import datetime
import functools
import logging
@@ -1549,18 +1550,31 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
self._state = False
-def lerp_color(
+def lerp_color_hsv(
rgb1: tuple[int, int, int],
rgb2: tuple[int, int, int],
t: float,
) -> tuple[int, int, int]:
- """Linearly interpolate between two RGB colors."""
- return (
- int(rgb1[0] + t * (rgb2[0] - rgb1[0])),
- int(rgb1[1] + t * (rgb2[1] - rgb1[1])),
- int(rgb1[2] + t * (rgb2[2] - rgb1[2])),
+ """Linearly interpolate between two RGB colors in HSV color space."""
+ t = abs(t)
+ assert 0 <= t <= 1
+
+ # Convert RGB to HSV
+ hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1])
+ hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2])
+
+ # Linear interpolation in HSV space
+ hsv = (
+ hsv1[0] + t * (hsv2[0] - hsv1[0]),
+ hsv1[1] + t * (hsv2[1] - hsv1[1]),
+ hsv1[2] + t * (hsv2[2] - hsv1[2]),
)
+ # Convert back to RGB
+ rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv))
+ assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}"
+ return rgb
+
@dataclass(frozen=True)
class SunLightSettings:
@@ -1748,7 +1762,7 @@ class SunLightSettings:
# This will result in a perceptible jump in color at sunset and sunrise
# because the `color_temperature_to_rgb` function is not 100% accurate.
min_color_rgb = color_temperature_to_rgb(self.min_color_temp)
- rgb_color = lerp_color(min_color_rgb, self.sleep_rgb_color, percent)
+ rgb_color = lerp_color_hsv(min_color_rgb, self.sleep_rgb_color, percent)
color_temp_kelvin = self.calc_color_temp_kelvin(percent)
force_rgb_color = True
else:
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 9b679a86..360fec55 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -92,6 +92,7 @@ from custom_components.adaptive_lighting.switch import (
create_context,
is_our_context,
is_our_context_id,
+ lerp_color_hsv,
)
_LOGGER = logging.getLogger(__name__)
@@ -1701,3 +1702,19 @@ async def test_adapt_until_sleep_and_rgb_colors(hass):
await switch._update_attrs_and_maybe_adapt_lights(context=context)
assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_SLEEP_BRIGHTNESS
assert switch._settings["rgb_color"] == DEFAULT_SLEEP_RGB_COLOR
+
+
+def test_lerp_color_hsv():
+ assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0) == (255, 0, 0)
+ assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 1) == (0, 255, 0)
+ assert lerp_color_hsv((255, 0, 0), (0, 255, 0), 0.5) == (255, 255, 0)
+ assert lerp_color_hsv((0, 0, 255), (255, 255, 255), 0.5) == (128, 255, 128)
+
+ # Tests that the interpolation is consistent
+ for t in [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]:
+ color = lerp_color_hsv((255, 0, 0), (0, 255, 0), t)
+ inverted_color = lerp_color_hsv((0, 255, 0), (255, 0, 0), 1 - t)
+ assert color == inverted_color
+
+ with pytest.raises(AssertionError):
+ lerp_color_hsv((255, 0, 0), (0, 255, 0), 1.1)
From 8696092879d4967dccefaec6472937a74e6c2616 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 25 Jul 2023 19:17:11 -0700
Subject: [PATCH 060/549] Avoid adapting lights with nothing in service_data,
closes #661 (#662)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 12 ++++++++++++
tests/test_switch.py | 8 ++++----
3 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 747fa2dd..1e232dab 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.17.1"
+ "version": "1.17.2"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index ce9f6922..6e337a30 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1159,6 +1159,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
use_transition = "transition" in features and transition > 0
if use_transition:
service_data[ATTR_TRANSITION] = transition
+
if "brightness" in features and adapt_brightness:
brightness = round(255 * self._settings["brightness_pct"] / 100)
service_data[ATTR_BRIGHTNESS] = brightness
@@ -1185,6 +1186,17 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
_LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light)
service_data[ATTR_RGB_COLOR] = self._settings["rgb_color"]
+ required_attrs = [ATTR_RGB_COLOR, ATTR_COLOR_TEMP_KELVIN, ATTR_BRIGHTNESS]
+ if not any(attr in service_data for attr in required_attrs):
+ _LOGGER.debug(
+ "%s: Skipping adaptation of %s because no relevant attributes"
+ " are set in service_data: %s",
+ self._name,
+ light,
+ service_data,
+ )
+ return None
+
context = context or self.create_context("adapt_lights")
self.manager.last_service_data[light] = service_data
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 360fec55..55aa30ea 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -1672,25 +1672,25 @@ async def test_adapt_until_sleep_and_rgb_colors(hass):
assert switch._settings["color_temp_kelvin"] > min_color_temp
assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT]
- # One hour after sunset the brightness should be down
+ # One hour after sunset the brightness should be down and use RGB
await patch_time_and_update(after_sunset)
assert switch._settings["force_rgb_color"]
assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS
assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT]
- # At sunrise the brightness should be max and color_temp at the smallest value
+ # At sunrise the brightness should be max and use Kelvin
await patch_time_and_update(sunrise)
assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
assert switch._settings["color_temp_kelvin"] == min_color_temp
assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT]
# One hour before sunrise the brightness should smaller than max
- # and color_temp at the min value.
+ # and use RGB
await patch_time_and_update(before_sunrise)
assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS
assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT]
- # One hour after sunrise the brightness should be up
+ # One hour after sunrise the brightness should be up and it should use Kelvin
await patch_time_and_update(after_sunrise)
assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
assert switch._settings["color_temp_kelvin"] > min_color_temp
From e9297d562f11c208b568c425b478974a9c48aa29 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 26 Jul 2023 12:52:03 -0700
Subject: [PATCH 061/549] Cleanup after add_to_platform_abort is called,
try-except intercept, update strings.json, fixes #658 (#659)
* Cleanup after add_to_platform_abort is called, fixes #658
* Add note
* try-catch because of priv
* Catch all exceptions
* Bump to 1.17.3 in manifest.json
* assert that hass is not None
* Rename _light_event_action -> _light_state_event_action
* update .github/update-strings.py
* Update README.md, strings.json, and services.yaml
* update name
* Fixes
* Update README.md, strings.json, and services.yaml
* Add name
* Do not add name
---------
Co-authored-by: github-actions[bot]
---
.github/update-strings.py | 24 ++-
.github/workflows/update-readme.yml | 8 +-
.../adaptive_lighting/hass_utils.py | 20 +-
.../adaptive_lighting/manifest.json | 2 +-
.../adaptive_lighting/strings.json | 176 ++++++++++++++++++
custom_components/adaptive_lighting/switch.py | 92 +++++----
.../adaptive_lighting/translations/en.json | 176 ++++++++++++++++++
7 files changed, 447 insertions(+), 51 deletions(-)
diff --git a/.github/update-strings.py b/.github/update-strings.py
index 8f8f9ef4..44dc6bd6 100644
--- a/.github/update-strings.py
+++ b/.github/update-strings.py
@@ -3,6 +3,8 @@ import json
import sys
from pathlib import Path
+import yaml
+
sys.path.append(str(Path(__file__).parent.parent))
from custom_components.adaptive_lighting import const # noqa: E402
@@ -13,20 +15,40 @@ en_fname = folder / "translations" / "en.json"
with strings_fname.open() as f:
strings = json.load(f)
+# Set "options"
data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES}
strings["options"]["step"]["init"]["data"] = data
+# Set "services"
+services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml"
+with open(services_filename) as f: # noqa: PTH123
+ services = yaml.safe_load(f)
+services_json = {}
+for service_name, dct in services.items():
+ services_json[service_name] = {
+ "name": service_name,
+ "description": dct["description"],
+ "fields": {},
+ }
+ for field_name, field in dct["fields"].items():
+ services_json[service_name]["fields"][field_name] = {
+ "description": field["description"],
+ "name": field_name,
+ }
+strings["services"] = services_json
+
+# Write changes to strings.json
with strings_fname.open("w") as f:
json.dump(strings, f, indent=2, ensure_ascii=False)
f.write("\n")
-
# Sync changes from strings.json to en.json
with en_fname.open() as f:
en = json.load(f)
en["config"]["step"]["user"] = strings["config"]["step"]["user"]
en["options"]["step"]["init"]["data"] = data
+en["services"] = services_json
with en_fname.open("w") as f:
json.dump(en, f, indent=2, ensure_ascii=False)
diff --git a/.github/workflows/update-readme.yml b/.github/workflows/update-readme.yml
index a31d556e..8fca3d77 100644
--- a/.github/workflows/update-readme.yml
+++ b/.github/workflows/update-readme.yml
@@ -1,4 +1,4 @@
-name: Update README.md
+name: Update README.md, strings.json, and services.yaml
on:
push:
@@ -34,12 +34,12 @@ jobs:
- name: Run markdown-code-runner
run: markdown-code-runner --debug README.md
- - name: Run update strings.json
- run: python .github/update-strings.py
-
- name: Run update services.yaml
run: python .github/update-services.py
+ - name: Run update strings.json
+ run: python .github/update-strings.py
+
- name: Commit updated README.md, strings.json, and services.yaml
id: commit
run: |
diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py
index 3f08b3fc..cb9b8ab4 100644
--- a/custom_components/adaptive_lighting/hass_utils.py
+++ b/custom_components/adaptive_lighting/hass_utils.py
@@ -1,4 +1,5 @@
"""Utility functions for HA core."""
+import logging
from collections.abc import Awaitable, Callable
from homeassistant.core import HomeAssistant, ServiceCall
@@ -6,6 +7,8 @@ from homeassistant.util.read_only_dict import ReadOnlyDict
from .adaptation_utils import ServiceData
+_LOGGER = logging.getLogger(__name__)
+
def setup_service_call_interceptor(
hass: HomeAssistant,
@@ -39,15 +42,18 @@ def setup_service_call_interceptor(
existing_service = registered_services[domain][service]
async def service_func_proxy(call: ServiceCall) -> None:
- # Convert read-only data to writeable dictionary for modification by interceptor
- data = dict(call.data)
+ try:
+ # Convert read-only data to writeable dictionary for modification by interceptor
+ data = dict(call.data)
- # Call interceptor
- await intercept_func(call, data)
-
- # Convert data back to read-only
- call.data = ReadOnlyDict(data)
+ # Call interceptor
+ await intercept_func(call, data)
+ # Convert data back to read-only
+ call.data = ReadOnlyDict(data)
+ except Exception as e: # noqa: BLE001
+ # Blindly catch all exceptions to avoid breaking light.turn_on
+ _LOGGER.error("Error in service_func_proxy: %s", e)
# Call original service handler with processed data
await existing_service.job.target(call)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 1e232dab..a5293b7a 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.17.2"
+ "version": "1.17.3"
}
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index 7ad84741..d142c7b7 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -56,5 +56,181 @@
"option_error": "Invalid option",
"entity_missing": "One or more selected light entities are missing from Home Assistant"
}
+ },
+ "services": {
+ "apply": {
+ "name": "apply",
+ "description": "Applies the current Adaptive Lighting settings to lights.",
+ "fields": {
+ "entity_id": {
+ "description": "The `entity_id` of the switch with the settings to apply. 📝",
+ "name": "entity_id"
+ },
+ "lights": {
+ "description": "A light (or list of lights) to apply the settings to. 💡",
+ "name": "lights"
+ },
+ "transition": {
+ "description": "Duration of transition when lights change, in seconds. 🕑",
+ "name": "transition"
+ },
+ "adapt_brightness": {
+ "description": "Whether to adapt the brightness of the light. 🌞",
+ "name": "adapt_brightness"
+ },
+ "adapt_color": {
+ "description": "Whether to adapt the color on supporting lights. 🌈",
+ "name": "adapt_color"
+ },
+ "prefer_rgb_color": {
+ "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
+ "name": "prefer_rgb_color"
+ },
+ "turn_on_lights": {
+ "description": "Whether to turn on lights that are currently off. 🔆",
+ "name": "turn_on_lights"
+ }
+ }
+ },
+ "set_manual_control": {
+ "name": "set_manual_control",
+ "description": "Mark whether a light is 'manually controlled'.",
+ "fields": {
+ "entity_id": {
+ "description": "The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝",
+ "name": "entity_id"
+ },
+ "lights": {
+ "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡",
+ "name": "lights"
+ },
+ "manual_control": {
+ "description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒",
+ "name": "manual_control"
+ }
+ }
+ },
+ "change_switch_settings": {
+ "name": "change_switch_settings",
+ "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.",
+ "fields": {
+ "entity_id": {
+ "description": "Entity ID of the switch. 📝",
+ "name": "entity_id"
+ },
+ "use_defaults": {
+ "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️",
+ "name": "use_defaults"
+ },
+ "include_config_in_attributes": {
+ "description": "Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
+ "name": "include_config_in_attributes"
+ },
+ "turn_on_lights": {
+ "description": "Whether to turn on lights that are currently off. 🔆",
+ "name": "turn_on_lights"
+ },
+ "initial_transition": {
+ "description": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
+ "name": "initial_transition"
+ },
+ "sleep_transition": {
+ "description": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
+ "name": "sleep_transition"
+ },
+ "max_brightness": {
+ "description": "Maximum brightness percentage. 💡",
+ "name": "max_brightness"
+ },
+ "max_color_temp": {
+ "description": "Coldest color temperature in Kelvin. ❄️",
+ "name": "max_color_temp"
+ },
+ "min_brightness": {
+ "description": "Minimum brightness percentage. 💡",
+ "name": "min_brightness"
+ },
+ "min_color_temp": {
+ "description": "Warmest color temperature in Kelvin. 🔥",
+ "name": "min_color_temp"
+ },
+ "only_once": {
+ "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
+ "name": "only_once"
+ },
+ "prefer_rgb_color": {
+ "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
+ "name": "prefer_rgb_color"
+ },
+ "separate_turn_on_commands": {
+ "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
+ "name": "separate_turn_on_commands"
+ },
+ "send_split_delay": {
+ "description": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
+ "name": "send_split_delay"
+ },
+ "sleep_brightness": {
+ "description": "Brightness percentage of lights in sleep mode. 😴",
+ "name": "sleep_brightness"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
+ "name": "sleep_rgb_or_color_temp"
+ },
+ "sleep_rgb_color": {
+ "description": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
+ "name": "sleep_rgb_color"
+ },
+ "sleep_color_temp": {
+ "description": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
+ "name": "sleep_color_temp"
+ },
+ "sunrise_offset": {
+ "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰",
+ "name": "sunrise_offset"
+ },
+ "sunrise_time": {
+ "description": "Set a fixed time (HH:MM:SS) for sunrise. 🌅",
+ "name": "sunrise_time"
+ },
+ "sunset_offset": {
+ "description": "Adjust sunset time with a positive or negative offset in seconds. ⏰",
+ "name": "sunset_offset"
+ },
+ "sunset_time": {
+ "description": "Set a fixed time (HH:MM:SS) for sunset. 🌇",
+ "name": "sunset_time"
+ },
+ "max_sunrise_time": {
+ "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
+ "name": "max_sunrise_time"
+ },
+ "min_sunset_time": {
+ "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
+ "name": "min_sunset_time"
+ },
+ "take_over_control": {
+ "description": "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
+ "name": "take_over_control"
+ },
+ "detect_non_ha_changes": {
+ "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
+ "name": "detect_non_ha_changes"
+ },
+ "transition": {
+ "description": "Duration of transition when lights change, in seconds. 🕑",
+ "name": "transition"
+ },
+ "adapt_delay": {
+ "description": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
+ "name": "adapt_delay"
+ },
+ "autoreset_control_seconds": {
+ "description": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "name": "autoreset_control_seconds"
+ }
+ }
+ }
}
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 6e337a30..6dae39f2 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -437,15 +437,13 @@ async def async_setup_entry( # noqa: PLR0915
async_add_entities: AddEntitiesCallback,
):
"""Set up the AdaptiveLighting switch."""
+ assert hass is not None
data = hass.data[DOMAIN]
assert config_entry.entry_id in data
-
- if ATTR_ADAPTIVE_LIGHTING_MANAGER not in data:
- data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = AdaptiveLightingManager(
- hass,
- config_entry,
- )
- manager: AdaptiveLightingManager = data[ATTR_ADAPTIVE_LIGHTING_MANAGER]
+ manager = data.setdefault(
+ ATTR_ADAPTIVE_LIGHTING_MANAGER,
+ AdaptiveLightingManager(hass, config_entry),
+ )
sleep_mode_switch = SimpleSwitch(
which="Sleep Mode",
initial_state=False,
@@ -503,7 +501,7 @@ async def async_setup_entry( # noqa: PLR0915
if not lights:
all_lights = switch.lights
else:
- all_lights = _expand_light_groups(switch.hass, lights)
+ all_lights = _expand_light_groups(hass, lights)
switch.manager.lights.update(all_lights)
for light in all_lights:
if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light):
@@ -533,7 +531,7 @@ async def async_setup_entry( # noqa: PLR0915
if not lights:
all_lights = switch.lights
else:
- all_lights = _expand_light_groups(switch.hass, lights)
+ all_lights = _expand_light_groups(hass, lights)
if service_call.data[CONF_MANUAL_CONTROL]:
for light in all_lights:
_fire_manual_control_event(switch, light, service_call.context)
@@ -585,7 +583,7 @@ def validate(
config_entry: ConfigEntry,
service_data: dict[str, Any] | None = None,
defaults: dict[str, Any] | None = None,
-):
+) -> dict[str, Any]:
"""Get the options and data from the config_entry and add defaults."""
if defaults is None:
data = {key: default for key, default, _ in VALIDATION_TUPLES}
@@ -812,6 +810,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
) -> None:
"""Initialize the Adaptive Lighting switch."""
# Set attributes that can't be modified during runtime
+ assert hass is not None
self.hass = hass
self.manager = manager
self.sleep_mode_switch = sleep_mode_switch
@@ -847,7 +846,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# Set and unset tracker in async_turn_on and async_turn_off
self.remove_listeners = []
self.remove_interval: Callable[[], None] = lambda: None
-
_LOGGER.debug(
"%s: Setting up with '%s',"
" config_entry.data: '%s',"
@@ -1005,7 +1003,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
remove_state = async_track_state_change_event(
self.hass,
entity_ids=self.lights,
- action=self._light_event_action,
+ action=self._light_state_event_action,
)
self.remove_listeners.append(remove_state)
@@ -1035,8 +1033,27 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
interval=adaptation_interval,
)
+ def _call_on_remove_callbacks(self) -> None:
+ """Call callbacks registered by async_on_remove."""
+ # This is called when the integration is removed from HA
+ # and in `Entity.add_to_platform_abort`.
+ # For some unknown reason (to me) `async_will_remove_from_hass`
+ # is not called in `add_to_platform_abort`.
+ # See https://github.com/basnijholt/adaptive-lighting/issues/658
+ self._remove_listeners()
+ try:
+ # HACK: this is a private method in `Entity` which can change
+ super()._call_on_remove_callbacks()
+ except AttributeError as err:
+ _LOGGER.error(
+ "%s: Caught AttributeError in `_call_on_remove_callbacks`: %s",
+ self._name,
+ err,
+ )
+
def _remove_interval_listener(self) -> None:
self.remove_interval()
+ self.remove_interval = lambda: None
def _remove_listeners(self) -> None:
self._remove_interval_listener()
@@ -1152,7 +1169,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
# Build service data.
- service_data = {ATTR_ENTITY_ID: light}
+ service_data: dict[str, Any] = {ATTR_ENTITY_ID: light}
features = _supported_features(self.hass, light)
# Check transition == 0 to fix #378
@@ -1289,8 +1306,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
to cancel an ongoing adaptation when a light is turned off.
"""
# Prevent overlap of multiple adaptation sequences
- listener = self.manager
- listener.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which)
+ self.manager.cancel_ongoing_adaptation_calls(data.entity_id, which=data.which)
_LOGGER.debug(
"%s: execute_cancellable_adaptation_calls with data: %s",
self._name,
@@ -1300,9 +1316,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
try:
task = asyncio.ensure_future(self._execute_adaptation_calls(data))
if data.which in ("both", "brightness"):
- listener.adaptation_tasks_brightness[data.entity_id] = task
+ self.manager.adaptation_tasks_brightness[data.entity_id] = task
if data.which in ("both", "color"):
- listener.adaptation_tasks_color[data.entity_id] = task
+ self.manager.adaptation_tasks_color[data.entity_id] = task
await task
except asyncio.CancelledError:
_LOGGER.debug(
@@ -1427,7 +1443,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context=self.create_context("sleep", parent=event.context),
)
- async def _light_event_action(self, event: Event) -> None:
+ async def _light_state_event_action(self, event: Event) -> None:
old_state = event.data.get("old_state")
new_state = event.data.get("new_state")
entity_id = event.data.get("entity_id")
@@ -1801,9 +1817,10 @@ class AdaptiveLightingManager:
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the AdaptiveLightingManager that is shared among all switches."""
+ assert hass is not None
self.hass = hass
data = validate(config_entry)
- self.lights = set()
+ self.lights: set[str] = set()
# Tracks 'light.turn_off' service calls
self.turn_off_event: dict[str, Event] = {}
@@ -1935,6 +1952,15 @@ class AdaptiveLightingManager:
return
entity_id = entity_ids[0]
+
+ # Prevent adaptation of TURN_ON calls when light is already on,
+ # and of TOGGLE calls when toggling off.
+ if self.hass.states.is_state(entity_id, STATE_ON):
+ return
+
+ if self.manual_control.get(entity_id, False):
+ return
+
try:
adaptive_switch = _switch_with_lights(self.hass, [entity_id])
except NoSwitchFoundError:
@@ -1952,14 +1978,6 @@ class AdaptiveLightingManager:
if entity_id not in adaptive_switch.lights:
return
- if self.manual_control.get(entity_id, False):
- return
-
- # Prevent adaptation of TURN_ON calls when light is already on,
- # and of TOGGLE calls when toggling off.
- if self.hass.states.is_state(entity_id, STATE_ON):
- return
-
_LOGGER.debug(
"Intercepted TURN_ON call with data %s (%s)",
data,
@@ -2143,11 +2161,10 @@ class AdaptiveLightingManager:
self.cancel_ongoing_adaptation_calls(light)
def _get_entity_list(self, service_data: ServiceData) -> list[str]:
- entity_ids = []
-
if ATTR_ENTITY_ID in service_data:
- entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID])
- elif ATTR_AREA_ID in service_data:
+ return cv.ensure_list_csv(service_data[ATTR_ENTITY_ID])
+ if ATTR_AREA_ID in service_data:
+ entity_ids = []
area_ids = cv.ensure_list_csv(service_data[ATTR_AREA_ID])
for area_id in area_ids:
area_entity_ids = area_entities(self.hass, area_id)
@@ -2162,13 +2179,12 @@ class AdaptiveLightingManager:
entity_ids,
area_id,
)
- else:
- _LOGGER.debug(
- "No entity_ids or area_ids found in service_data: %s",
- service_data,
- )
-
- return entity_ids
+ return entity_ids
+ _LOGGER.debug(
+ "No entity_ids or area_ids found in service_data: %s",
+ service_data,
+ )
+ return []
async def turn_on_off_event_listener(self, event: Event) -> None:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index e6d1f9a9..199c333c 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -57,5 +57,181 @@
"option_error": "Invalid option",
"entity_missing": "One or more selected light entities are missing from Home Assistant"
}
+ },
+ "services": {
+ "apply": {
+ "name": "apply",
+ "description": "Applies the current Adaptive Lighting settings to lights.",
+ "fields": {
+ "entity_id": {
+ "description": "The `entity_id` of the switch with the settings to apply. 📝",
+ "name": "entity_id"
+ },
+ "lights": {
+ "description": "A light (or list of lights) to apply the settings to. 💡",
+ "name": "lights"
+ },
+ "transition": {
+ "description": "Duration of transition when lights change, in seconds. 🕑",
+ "name": "transition"
+ },
+ "adapt_brightness": {
+ "description": "Whether to adapt the brightness of the light. 🌞",
+ "name": "adapt_brightness"
+ },
+ "adapt_color": {
+ "description": "Whether to adapt the color on supporting lights. 🌈",
+ "name": "adapt_color"
+ },
+ "prefer_rgb_color": {
+ "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
+ "name": "prefer_rgb_color"
+ },
+ "turn_on_lights": {
+ "description": "Whether to turn on lights that are currently off. 🔆",
+ "name": "turn_on_lights"
+ }
+ }
+ },
+ "set_manual_control": {
+ "name": "set_manual_control",
+ "description": "Mark whether a light is 'manually controlled'.",
+ "fields": {
+ "entity_id": {
+ "description": "The `entity_id` of the switch in which to (un)mark the light as being `manually controlled`. 📝",
+ "name": "entity_id"
+ },
+ "lights": {
+ "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. 💡",
+ "name": "lights"
+ },
+ "manual_control": {
+ "description": "Whether to add (\"true\") or remove (\"false\") the light from the \"manual_control\" list. 🔒",
+ "name": "manual_control"
+ }
+ }
+ },
+ "change_switch_settings": {
+ "name": "change_switch_settings",
+ "description": "Change any settings you'd like in the switch. All options here are the same as in the config flow.",
+ "fields": {
+ "entity_id": {
+ "description": "Entity ID of the switch. 📝",
+ "name": "entity_id"
+ },
+ "use_defaults": {
+ "description": "Sets the default values not specified in this service call. Options: \"current\" (default, retains current values), \"factory\" (resets to documented defaults), or \"configuration\" (reverts to switch config defaults). ⚙️",
+ "name": "use_defaults"
+ },
+ "include_config_in_attributes": {
+ "description": "Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
+ "name": "include_config_in_attributes"
+ },
+ "turn_on_lights": {
+ "description": "Whether to turn on lights that are currently off. 🔆",
+ "name": "turn_on_lights"
+ },
+ "initial_transition": {
+ "description": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
+ "name": "initial_transition"
+ },
+ "sleep_transition": {
+ "description": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
+ "name": "sleep_transition"
+ },
+ "max_brightness": {
+ "description": "Maximum brightness percentage. 💡",
+ "name": "max_brightness"
+ },
+ "max_color_temp": {
+ "description": "Coldest color temperature in Kelvin. ❄️",
+ "name": "max_color_temp"
+ },
+ "min_brightness": {
+ "description": "Minimum brightness percentage. 💡",
+ "name": "min_brightness"
+ },
+ "min_color_temp": {
+ "description": "Warmest color temperature in Kelvin. 🔥",
+ "name": "min_color_temp"
+ },
+ "only_once": {
+ "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
+ "name": "only_once"
+ },
+ "prefer_rgb_color": {
+ "description": "Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
+ "name": "prefer_rgb_color"
+ },
+ "separate_turn_on_commands": {
+ "description": "Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
+ "name": "separate_turn_on_commands"
+ },
+ "send_split_delay": {
+ "description": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
+ "name": "send_split_delay"
+ },
+ "sleep_brightness": {
+ "description": "Brightness percentage of lights in sleep mode. 😴",
+ "name": "sleep_brightness"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
+ "name": "sleep_rgb_or_color_temp"
+ },
+ "sleep_rgb_color": {
+ "description": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
+ "name": "sleep_rgb_color"
+ },
+ "sleep_color_temp": {
+ "description": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
+ "name": "sleep_color_temp"
+ },
+ "sunrise_offset": {
+ "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰",
+ "name": "sunrise_offset"
+ },
+ "sunrise_time": {
+ "description": "Set a fixed time (HH:MM:SS) for sunrise. 🌅",
+ "name": "sunrise_time"
+ },
+ "sunset_offset": {
+ "description": "Adjust sunset time with a positive or negative offset in seconds. ⏰",
+ "name": "sunset_offset"
+ },
+ "sunset_time": {
+ "description": "Set a fixed time (HH:MM:SS) for sunset. 🌇",
+ "name": "sunset_time"
+ },
+ "max_sunrise_time": {
+ "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
+ "name": "max_sunrise_time"
+ },
+ "min_sunset_time": {
+ "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
+ "name": "min_sunset_time"
+ },
+ "take_over_control": {
+ "description": "Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
+ "name": "take_over_control"
+ },
+ "detect_non_ha_changes": {
+ "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
+ "name": "detect_non_ha_changes"
+ },
+ "transition": {
+ "description": "Duration of transition when lights change, in seconds. 🕑",
+ "name": "transition"
+ },
+ "adapt_delay": {
+ "description": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
+ "name": "adapt_delay"
+ },
+ "autoreset_control_seconds": {
+ "description": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "name": "autoreset_control_seconds"
+ }
+ }
+ }
}
}
From 1cda675bbef3a987da434f2b71fc94c5f42fd5c3 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 26 Jul 2023 15:01:29 -0700
Subject: [PATCH 062/549] Add @bind_hass to functions (#667)
---
custom_components/adaptive_lighting/switch.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 6dae39f2..6bba8afe 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -78,6 +78,7 @@ from homeassistant.helpers.event import (
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.sun import get_astral_location
from homeassistant.helpers.template import area_entities
+from homeassistant.loader import bind_hass
from homeassistant.util import slugify
from homeassistant.util.color import (
color_RGB_to_xy,
@@ -274,6 +275,7 @@ def is_our_context(context: Context | None) -> bool:
return is_our_context_id(context.id)
+@bind_hass
def _switches_with_lights(
hass: HomeAssistant,
lights: list[str],
@@ -299,6 +301,7 @@ class NoSwitchFoundError(ValueError):
"""No switches found for lights."""
+@bind_hass
def _switch_with_lights(
hass: HomeAssistant,
lights: list[str],
@@ -328,6 +331,7 @@ def _switch_with_lights(
# For documentation on this function, see integration_entities() from HomeAssistant Core:
# https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/template.py#L1109
+@bind_hass
def _switches_from_service_call(
hass: HomeAssistant,
service_call: ServiceCall,
@@ -617,6 +621,7 @@ def _is_state_event(event: Event, from_or_to_state: Iterable[str]):
)
+@bind_hass
def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
all_lights = set()
manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER]
@@ -635,6 +640,7 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
return list(all_lights)
+@bind_hass
def _supported_features(hass: HomeAssistant, light: str) -> set[str]:
state = hass.states.get(light)
supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
From c64d9cefbed1acf93b229684f852bfe44913bbf8 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 26 Jul 2023 23:40:54 -0700
Subject: [PATCH 063/549] Delete AdaptiveLighting instances that have been
removed from YAML (#669)
* Do not re-add already added configs
* Use async_remove
---
.../adaptive_lighting/config_flow.py | 5 ++++-
custom_components/adaptive_lighting/switch.py | 17 +++++++++++++++++
2 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py
index 170c8503..3dd89fd4 100644
--- a/custom_components/adaptive_lighting/config_flow.py
+++ b/custom_components/adaptive_lighting/config_flow.py
@@ -40,10 +40,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
)
async def async_step_import(self, user_input=None):
- """Handle configuration by yaml file."""
+ """Handle configuration by YAML file."""
await self.async_set_unique_id(user_input[CONF_NAME])
for entry in self._async_current_entries():
if entry.unique_id == self.unique_id:
+ # Keep a list of switches that are configured via YAML
+ data = self.hass.data.setdefault(DOMAIN, {})
+ data.setdefault("__yaml__", []).append(self.unique_id)
self.hass.config_entries.async_update_entry(entry, data=user_input)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 6bba8afe..72b53635 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -42,6 +42,7 @@ from homeassistant.components.light import (
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.switch import SwitchEntity
+from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_AREA_ID,
ATTR_DOMAIN,
@@ -444,6 +445,22 @@ async def async_setup_entry( # noqa: PLR0915
assert hass is not None
data = hass.data[DOMAIN]
assert config_entry.entry_id in data
+ _LOGGER.debug(
+ "Setting up AdaptiveLighting with data: %s and config_entry %s",
+ data,
+ config_entry,
+ )
+ if ( # Skip deleted YAML config entries
+ config_entry.source == SOURCE_IMPORT
+ and config_entry.unique_id not in data.get("__yaml__", [])
+ ):
+ _LOGGER.warning(
+ "Deleting AdaptiveLighting switch '%s' because YAML"
+ " defined switch has been removed from YAML configuration",
+ config_entry.unique_id,
+ )
+ await hass.config_entries.async_remove(config_entry.entry_id)
+ return
manager = data.setdefault(
ATTR_ADAPTIVE_LIGHTING_MANAGER,
AdaptiveLightingManager(hass, config_entry),
From 9e15d1bd22593aa11923332e4123181ba8d446de Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 27 Jul 2023 09:51:59 -0700
Subject: [PATCH 064/549] Update to 1.17.4 in manifest.json (#670)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index a5293b7a..4ba4790c 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.17.3"
+ "version": "1.17.4"
}
From a6e987438df1d9e50963900dc0477248d74facb1 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Thu, 27 Jul 2023 17:22:12 +0000
Subject: [PATCH 065/549] [pre-commit.ci] pre-commit autoupdate (#627)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.279 → v0.0.280](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.279...v0.0.280)
- [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt
---
.pre-commit-config.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6418f6a3..dc82bbe7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,11 +8,11 @@ repos:
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.0.279
+ rev: v0.0.280
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/psf/black
- rev: 23.3.0
+ rev: 23.7.0
hooks:
- id: black
From f699486fd0752c99df4abb551fa77a65e918bb1f Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 27 Jul 2023 17:34:56 -0700
Subject: [PATCH 066/549] Only cancel ongoing adaptation calls if still running
(#668)
---
custom_components/adaptive_lighting/switch.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 72b53635..24304730 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -2151,7 +2151,11 @@ class AdaptiveLightingManager:
"""Cancel ongoing adaptation service calls for a specific light entity."""
brightness_task = self.adaptation_tasks_brightness.get(light_id)
color_task = self.adaptation_tasks_color.get(light_id)
- if which in ("both", "brightness") and brightness_task is not None:
+ if (
+ which in ("both", "brightness")
+ and brightness_task is not None
+ and not brightness_task.done()
+ ):
_LOGGER.debug(
"Cancelled ongoing brightness adaptation calls (%s) for '%s'",
brightness_task,
@@ -2162,6 +2166,7 @@ class AdaptiveLightingManager:
which in ("both", "color")
and color_task is not None
and color_task is not brightness_task
+ and not color_task.done()
):
_LOGGER.debug(
"Cancelled ongoing color adaptation calls (%s) for '%s'",
From 9d6f93538f1ef6e687d240987aedc8b6d3ad2b5f Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 27 Jul 2023 18:01:06 -0700
Subject: [PATCH 067/549] Do not create multiple AdaptiveLightingManager
instances (#672)
I didn't realize that setdefault always executes the default.
---
custom_components/adaptive_lighting/switch.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 24304730..bd530f36 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -285,12 +285,12 @@ def _switches_with_lights(
config_entries = hass.config_entries.async_entries(DOMAIN)
data = hass.data[DOMAIN]
switches = []
+ all_check_lights = _expand_light_groups(hass, lights)
for config in config_entries:
entry = data.get(config.entry_id)
if entry is None: # entry might be disabled and therefore missing
continue
switch = data[config.entry_id]["instance"]
- all_check_lights = _expand_light_groups(hass, lights)
switch._expand_light_groups()
# Check if any of the lights are in the switch's lights
if set(switch.lights) & set(all_check_lights):
@@ -461,10 +461,11 @@ async def async_setup_entry( # noqa: PLR0915
)
await hass.config_entries.async_remove(config_entry.entry_id)
return
- manager = data.setdefault(
- ATTR_ADAPTIVE_LIGHTING_MANAGER,
- AdaptiveLightingManager(hass, config_entry),
- )
+
+ if (manager := data.get(ATTR_ADAPTIVE_LIGHTING_MANAGER)) is None:
+ manager = AdaptiveLightingManager(hass, config_entry)
+ data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = manager
+
sleep_mode_switch = SimpleSwitch(
which="Sleep Mode",
initial_state=False,
From f23aeb269bf9150a17c925884d52d5d742dd2cb1 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 27 Jul 2023 18:09:37 -0700
Subject: [PATCH 068/549] Bump to 1.17.5
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 4ba4790c..8695c555 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.17.4"
+ "version": "1.17.5"
}
From 89c90adde040dab49882416e1dcb270212423411 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 28 Jul 2023 15:17:17 -0700
Subject: [PATCH 069/549] Only start adapting on `light.turn_on` when
`detect_non_ha_changes: false` to prevent unwanted light turn ons (#663)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fixes in maybe_cancel_adjusting to possibly fix accidental turn on
* simplify logic in maybe_cancel_adjusting
* Add logging statements
* only control if turn_on called
* more logs
* add TODO
* Add _state_event_is_from_our_turn_on
* Ignore off->on state switches that are not accociated with light.turn_on
* improve logging
* rename
* Update docs
* Update README.md, strings.json, and services.yaml
* Add caution message to README
* Change order of emojis
* Update README.md, strings.json, and services.yaml
* Check that platform is not None
* log the call
* fix args
* Do not re-add already added configs
* Do not re-add already added configs
* Use async_remove
* remove unused code
* [pre-commit.ci] pre-commit autoupdate (#627)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.279 → v0.0.280](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.279...v0.0.280)
- [github.com/psf/black: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt
* Extra logging statement
* Add to README
* log context_id
* return right indent
* move comment
* Skip on self.manager.is_proactively_adapting
---------
Co-authored-by: github-actions[bot]
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
README.md | 74 +++----
custom_components/adaptive_lighting/const.py | 7 +-
.../adaptive_lighting/hass_utils.py | 6 +-
.../adaptive_lighting/services.yaml | 2 +-
.../adaptive_lighting/strings.json | 4 +-
custom_components/adaptive_lighting/switch.py | 182 +++++++++++-------
.../adaptive_lighting/translations/en.json | 4 +-
tests/test_switch.py | 2 +-
8 files changed, 171 insertions(+), 110 deletions(-)
diff --git a/README.md b/README.md
index 20a21dad..d1d7aa16 100644
--- a/README.md
+++ b/README.md
@@ -33,9 +33,11 @@ Adaptive Lighting is designed to automatically detect when you or another source
When this occurs, the affected light is marked as "manually controlled," and Adaptive Lighting will not make further adjustments until the light is turned off and back on or reset using the `adaptive_lighting.set_manual_control` service call.
This feature is available when `take_over_control` is enabled.
-Additionally, enabling detect_non_ha_changes allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings.
+Additionally, enabling `detect_non_ha_changes` allows Adaptive Lighting to detect all state changes, including those made outside of Home Assistant, by comparing the light's state to its previously used settings.
The `adaptive_lighting.manual_control` event is fired when a light is marked as "manually controlled," allowing for integration with automations 🤖.
+> ⚠️ **_Caution: Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable `detect_non_ha_changes` if you encounter such issues._**
+
## :books: Table of Contents
@@ -91,38 +93,38 @@ The YAML and frontend configuration methods support all of the options listed be
-| Variable name | Description | Default | Type |
-|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
-| `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` |
-| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
-| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
-| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
-| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
-| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
-| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
-| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
-| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
-| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
-| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
-| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
-| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
-| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
-| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
-| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
-| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
-| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
-| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
-| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
-| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
-| `detect_non_ha_changes` | Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️ | `False` | `bool` |
-| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
-| `send_split_delay` | 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 |
-| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
+| Variable name | Description | Default | Type |
+|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
+| `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` |
+| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
+| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
+| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
+| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
+| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
+| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
+| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
+| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
+| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
+| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
+| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
+| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
+| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
+| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
+| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
+| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
+| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
+| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
+| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
+| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
+| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` |
+| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
+| `send_split_delay` | 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 |
+| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
@@ -341,7 +343,11 @@ Adaptive Lighting sends more commands to lights than a typical human user would.
- Unresponsive lights.
- Home Assistant reporting incorrect light states, causing Adaptive Lighting to inadvertently turn lights back on.
-Most issues that appear to be caused by Adaptive Lighting are actually due to unrelated problems. Addressing these issues will significantly improve your Home Assistant experience.
+Most issues that appear to be caused by Adaptive Lighting are actually due to unrelated problems.
+Addressing these issues will significantly improve your Home Assistant experience.
+
+In case lights are suddenly turning on by themselves, this is most likely due to the light incorrectly reporting an "on" state to Home Assistant, leading to an undesired Adaptive Lighting action.
+To prevent adapting in cases *where the state of the light is suddenly "on" and only adapt if there is an associated `light.turn_on` service call*, set `detect_non_ha_changes: false`.
#### :signal_strength: WiFi Networks
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index 64b96583..94e9a50b 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -29,8 +29,11 @@ CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = (
False,
)
DOCS[CONF_DETECT_NON_HA_CHANGES] = (
- "Detect non-`light.turn_on` state changes and stop adapting lights. "
- "Requires `take_over_control`. 🕵️"
+ "Detects and halts adaptations for non-`light.turn_on` state changes. "
+ "Needs `take_over_control` enabled. 🕵️ "
+ "Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result "
+ "in lights turning on unexpectedly. "
+ "Disable this feature if you encounter such issues."
)
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES = (
diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py
index cb9b8ab4..ba5bb8b0 100644
--- a/custom_components/adaptive_lighting/hass_utils.py
+++ b/custom_components/adaptive_lighting/hass_utils.py
@@ -53,7 +53,11 @@ def setup_service_call_interceptor(
call.data = ReadOnlyDict(data)
except Exception as e: # noqa: BLE001
# Blindly catch all exceptions to avoid breaking light.turn_on
- _LOGGER.error("Error in service_func_proxy: %s", e)
+ _LOGGER.error(
+ "Error for call '%s' in service_func_proxy: '%s'",
+ call.data,
+ e,
+ )
# Call original service handler with processed data
await existing_service.job.target(call)
diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml
index cd25811b..5b857b32 100644
--- a/custom_components/adaptive_lighting/services.yaml
+++ b/custom_components/adaptive_lighting/services.yaml
@@ -226,7 +226,7 @@ change_switch_settings:
selector:
boolean: null
detect_non_ha_changes:
- description: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️
+ description: 'Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an ''on'' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.'
required: false
example: false
selector:
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index d142c7b7..b6d62b0b 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -43,7 +43,7 @@
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
- "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
+ "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
@@ -215,7 +215,7 @@
"name": "take_over_control"
},
"detect_non_ha_changes": {
- "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
+ "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
"name": "detect_non_ha_changes"
},
"transition": {
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index bd530f36..7cacf435 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -594,6 +594,7 @@ async def async_setup_entry( # noqa: PLR0915
if k not in skip:
args[vol.Optional(k)] = valid
platform = entity_platform.current_platform.get()
+ assert platform is not None
platform.async_register_entity_service(
SERVICE_CHANGE_SWITCH_SETTINGS,
args,
@@ -855,9 +856,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._icon = ICON_MAIN
self._state = None
- # Tracks 'off' → 'on' state changes
- self._on_to_off_event: dict[str, Event] = {}
# Tracks 'on' → 'off' state changes
+ self._on_to_off_event: dict[str, Event] = {}
+ # Tracks 'off' → 'on' state changes
self._off_to_on_event: dict[str, Event] = {}
# Locks that prevent light adjusting when waiting for a light to 'turn_off'
self._locks: dict[str, asyncio.Lock] = {}
@@ -1443,10 +1444,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
else:
_LOGGER.debug(
"%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:"
- " '%s' with transition %s",
+ " '%s' with transition %s and context.id=%s",
self._name,
light,
transition,
+ context.id,
)
await self._adapt_light(light, transition, context=context)
@@ -1471,12 +1473,24 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
old_state = event.data.get("old_state")
new_state = event.data.get("new_state")
entity_id = event.data.get("entity_id")
- if (
- old_state is not None
- and old_state.state == STATE_OFF
- and new_state is not None
- and new_state.state == STATE_ON
- ):
+
+ if old_state is None or new_state is None:
+ return
+
+ if old_state.state == STATE_ON and new_state.state == STATE_OFF:
+ # Tracks 'on' → 'off' state changes
+ self._on_to_off_event[entity_id] = event
+ self.manager.reset(entity_id)
+ _LOGGER.debug(
+ "%s: Detected an 'on' → 'off' event for '%s' with context.id='%s'",
+ self._name,
+ entity_id,
+ event.context.id,
+ )
+
+ if old_state.state == STATE_OFF and new_state.state == STATE_ON:
+ # Tracks 'off' → 'on' state changes
+ self._off_to_on_event[entity_id] = event
_LOGGER.debug(
"%s: Detected an 'off' → 'on' event for '%s' with context.id='%s'",
self._name,
@@ -1484,13 +1498,36 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
event.context.id,
)
+ if (
+ not self._detect_non_ha_changes
+ and not self.manager.is_proactively_adapting(event.context.id)
+ and not self.manager._off_to_on_state_event_is_from_turn_on(
+ entity_id,
+ event,
+ )
+ ):
+ # If we don't detect non-HA changes, we're only adjusting lights that
+ # were turned on by HA. If the light was turned on by something else,
+ # we don't adjust it (e.g., when HA suddenly reports it as on).
+ # Sometimes the light incorrectly reports itself as on when it's
+ # actually off. This code path will ensure that the light is
+ # not controlled by Adaptive Lighting.
+ _LOGGER.debug(
+ "%s: Ignoring 'off' → 'on' event for '%s' with context.id='%s'"
+ " because 'light.turn_on' was not called by HA and"
+ " 'detect_non_ha_changes' is False",
+ self._name,
+ entity_id,
+ event.context.id,
+ )
+ self.manager.mark_as_manual_control(entity_id)
+ return
+
if event.context.parent_id and not self.manager.is_proactively_adapting(
event.context.id,
):
self.manager.reset(entity_id, reset_manual_control=False)
- # Tracks 'off' → 'on' state changes
- self._off_to_on_event[entity_id] = event
lock = self._locks.setdefault(entity_id, asyncio.Lock())
async with lock:
if await self.manager.maybe_cancel_adjusting(
@@ -1527,15 +1564,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
force=True,
context=self.create_context("light_event", parent=event.context),
)
- elif (
- old_state is not None
- and old_state.state == STATE_ON
- and new_state is not None
- and new_state.state == STATE_OFF
- ):
- # Tracks 'off' → 'on' state changes
- self._on_to_off_event[entity_id] = event
- self.manager.reset(entity_id)
class SimpleSwitch(SwitchEntity, RestoreEntity):
@@ -2365,6 +2393,8 @@ class AdaptiveLightingManager:
detected, we mark the light as 'manually controlled' until the light
or switch is turned 'off' and 'on' again.
"""
+ assert switch._detect_non_ha_changes
+
last_service_data = self.last_service_data.get(light)
if last_service_data is None:
return None
@@ -2379,44 +2409,49 @@ class AdaptiveLightingManager:
# Ensure HASS is correctly updating your light's state with
# light.turn_on calls if any problems arise. This
# can happen e.g. using zigbee2mqtt with 'report: false' in device settings.
- if switch._detect_non_ha_changes:
+ await self.hass.helpers.entity_component.async_update_entity(light)
+ refreshed_state = self.hass.states.get(light)
+
+ changed = compare_to(
+ old_attributes=last_service_data,
+ new_attributes=refreshed_state.attributes,
+ )
+ if changed:
_LOGGER.debug(
- "%s: 'detect_non_ha_changes: true', calling update_entity(%s)"
- " and check if it's last adapt succeeded.",
+ "%s: State attributes of '%s' (%s) didn't change wrt 'last_service_data' (%s) (context.id=%s)",
switch._name,
light,
+ refreshed_state.attributes,
+ last_service_data,
+ context.id,
)
- # This update_entity probably isn't necessary now that we're checking
- # if transitions finished from our last adapt.
- await self.hass.helpers.entity_component.async_update_entity(light)
- refreshed_state = self.hass.states.get(light)
- _LOGGER.debug(
- "%s: Current state of %s: %s",
- switch._name,
- light,
- refreshed_state,
- )
- changed = compare_to(
- old_attributes=last_service_data,
- new_attributes=refreshed_state.attributes,
- )
- if changed:
- _LOGGER.debug(
- "State of '%s' didn't change wrt 'last_service_data' (context.id=%s)",
- light,
- context.id,
- )
- return True
+ return True
_LOGGER.debug(
- "%s: Light '%s' correctly matches our last adapt's service data, continuing..."
- " context.id=%s.",
+ "%s: State attributes of '%s' (%s) changed wrt 'last_service_data' (%s) (context.id=%s)",
switch._name,
light,
+ refreshed_state.attributes,
+ last_service_data,
context.id,
)
return False
- async def maybe_cancel_adjusting( # noqa: PLR0911, PLR0912
+ def _off_to_on_state_event_is_from_turn_on(
+ self,
+ entity_id: str,
+ off_to_on_event: Event,
+ ) -> bool:
+ # Adaptive Lighting should never turn on lights itself
+ assert not is_our_context(off_to_on_event.context)
+ turn_on_event: Event | None = self.turn_on_event.get(entity_id)
+ id_off_to_on = off_to_on_event.context.id
+ return (
+ turn_on_event is not None
+ and id_off_to_on is not None
+ and id_off_to_on == turn_on_event.context.id
+ )
+
+ async def maybe_cancel_adjusting( # noqa: PLR0911
self,
entity_id: str,
off_to_on_event: Event,
@@ -2435,7 +2470,11 @@ class AdaptiveLightingManager:
adjust the lights.
"""
if on_to_off_event is None:
- # No state change has been registered before.
+ _LOGGER.debug(
+ "maybe_cancel_adjusting: No 'on' → 'off' state change has been registered before for '%s'."
+ " It's possible that the light was already on when Home Assistant was turned on.",
+ entity_id,
+ )
return False
id_on_to_off = on_to_off_event.context.id
@@ -2446,18 +2485,10 @@ class AdaptiveLightingManager:
else:
transition = None
- turn_on_event = self.turn_on_event.get(entity_id)
- if turn_on_event is None:
- # This means that the light never got a 'turn_on' call that we
- # registered. I am not 100% sure why this happens, but it does.
- # This is a fix for #170 and #232.
- return False
- id_turn_on = turn_on_event.context.id
-
- id_off_to_on = off_to_on_event.context.id
-
- if id_off_to_on == id_turn_on and id_off_to_on is not None:
- # State change 'off' → 'on' triggered by 'light.turn_on'.
+ if self._off_to_on_state_event_is_from_turn_on(entity_id, off_to_on_event):
+ _LOGGER.debug(
+ "maybe_cancel_adjusting: State change 'off' → 'on' triggered by 'light.turn_on'",
+ )
return False
if (
@@ -2476,6 +2507,11 @@ class AdaptiveLightingManager:
delta_time = (dt_util.utcnow() - on_to_off_event.time_fired).total_seconds()
if delta_time > delay:
+ _LOGGER.debug(
+ "maybe_cancel_adjusting: delta_time='%s' > delay='%s'",
+ delta_time,
+ delay,
+ )
return False
# Here we could just `return True` but because we want to prevent any updates
@@ -2484,23 +2520,33 @@ class AdaptiveLightingManager:
# is 'off' or the time has passed.
delay -= delta_time # delta_time has passed since the 'off' → 'on' event
- _LOGGER.debug("Waiting with adjusting '%s' for %s", entity_id, delay)
-
+ _LOGGER.debug(
+ "maybe_cancel_adjusting: Waiting with adjusting '%s' for %s",
+ entity_id,
+ delay,
+ )
+ total_sleep = 0
for _ in range(3):
# It can happen that the actual transition time is longer than the
# specified time in the 'turn_off' service.
coro = asyncio.sleep(delay)
+ total_sleep += delay
task = self.sleep_tasks[entity_id] = asyncio.ensure_future(coro)
try:
await task
except asyncio.CancelledError: # 'light.turn_on' has been called
_LOGGER.debug(
- "Sleep task is cancelled due to 'light.turn_on('%s')' call",
+ "maybe_cancel_adjusting: Sleep task is cancelled due to 'light.turn_on('%s')' call",
entity_id,
)
return False
if not is_on(self.hass, entity_id):
+ _LOGGER.debug(
+ "maybe_cancel_adjusting: '%s' is off after %s seconds, cancelling adaptation",
+ entity_id,
+ total_sleep,
+ )
return True
delay = TURNING_OFF_DELAY # next time only wait this long
@@ -2511,10 +2557,12 @@ class AdaptiveLightingManager:
return True
# Now we assume that the lights are still on and they were intended
- # to be on. In case this still gives problems for some, we might
- # choose to **only** adapt on 'light.turn_on' events and ignore
- # other 'off' → 'on' state switches resulting from polling. That
- # would mean we 'return True' here.
+ # to be on.
+ _LOGGER.debug(
+ "maybe_cancel_adjusting: '%s' is still on after %s seconds, assuming it was intended to be on",
+ entity_id,
+ total_sleep,
+ )
return False
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index 199c333c..358f7cbe 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -44,7 +44,7 @@
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
- "detect_non_ha_changes": "detect_non_ha_changes: Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
+ "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
@@ -216,7 +216,7 @@
"name": "take_over_control"
},
"detect_non_ha_changes": {
- "description": "Detect non-`light.turn_on` state changes and stop adapting lights. Requires `take_over_control`. 🕵️",
+ "description": "Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
"name": "detect_non_ha_changes"
},
"transition": {
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 55aa30ea..328e493f 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -1478,7 +1478,7 @@ async def test_proactive_adaptation_with_separate_commands(hass):
)
# Expect two service calls
- assert len(event_context_ids) == 2
+ assert len(event_context_ids) == 2, event_context_ids
assert event_context_ids[0] == "test_context"
assert is_our_context_id(event_context_ids[1])
From 416f1eb2f15e4731f3a2771280b0109dcce32b36 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 28 Jul 2023 15:22:16 -0700
Subject: [PATCH 070/549] Rename maybe_cancel_adjusting to just_turned_off
(#673)
---
custom_components/adaptive_lighting/switch.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 7cacf435..129f7626 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1530,7 +1530,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
lock = self._locks.setdefault(entity_id, asyncio.Lock())
async with lock:
- if await self.manager.maybe_cancel_adjusting(
+ if await self.manager.just_turned_off(
entity_id,
off_to_on_event=event,
on_to_off_event=self._on_to_off_event.get(entity_id),
@@ -2451,7 +2451,7 @@ class AdaptiveLightingManager:
and id_off_to_on == turn_on_event.context.id
)
- async def maybe_cancel_adjusting( # noqa: PLR0911
+ async def just_turned_off( # noqa: PLR0911
self,
entity_id: str,
off_to_on_event: Event,
@@ -2471,7 +2471,7 @@ class AdaptiveLightingManager:
"""
if on_to_off_event is None:
_LOGGER.debug(
- "maybe_cancel_adjusting: No 'on' → 'off' state change has been registered before for '%s'."
+ "just_turned_off: No 'on' → 'off' state change has been registered before for '%s'."
" It's possible that the light was already on when Home Assistant was turned on.",
entity_id,
)
@@ -2487,7 +2487,7 @@ class AdaptiveLightingManager:
if self._off_to_on_state_event_is_from_turn_on(entity_id, off_to_on_event):
_LOGGER.debug(
- "maybe_cancel_adjusting: State change 'off' → 'on' triggered by 'light.turn_on'",
+ "just_turned_off: State change 'off' → 'on' triggered by 'light.turn_on'",
)
return False
@@ -2508,7 +2508,7 @@ class AdaptiveLightingManager:
delta_time = (dt_util.utcnow() - on_to_off_event.time_fired).total_seconds()
if delta_time > delay:
_LOGGER.debug(
- "maybe_cancel_adjusting: delta_time='%s' > delay='%s'",
+ "just_turned_off: delta_time='%s' > delay='%s'",
delta_time,
delay,
)
@@ -2521,7 +2521,7 @@ class AdaptiveLightingManager:
delay -= delta_time # delta_time has passed since the 'off' → 'on' event
_LOGGER.debug(
- "maybe_cancel_adjusting: Waiting with adjusting '%s' for %s",
+ "just_turned_off: Waiting with adjusting '%s' for %s",
entity_id,
delay,
)
@@ -2536,14 +2536,14 @@ class AdaptiveLightingManager:
await task
except asyncio.CancelledError: # 'light.turn_on' has been called
_LOGGER.debug(
- "maybe_cancel_adjusting: Sleep task is cancelled due to 'light.turn_on('%s')' call",
+ "just_turned_off: Sleep task is cancelled due to 'light.turn_on('%s')' call",
entity_id,
)
return False
if not is_on(self.hass, entity_id):
_LOGGER.debug(
- "maybe_cancel_adjusting: '%s' is off after %s seconds, cancelling adaptation",
+ "just_turned_off: '%s' is off after %s seconds, cancelling adaptation",
entity_id,
total_sleep,
)
@@ -2559,7 +2559,7 @@ class AdaptiveLightingManager:
# Now we assume that the lights are still on and they were intended
# to be on.
_LOGGER.debug(
- "maybe_cancel_adjusting: '%s' is still on after %s seconds, assuming it was intended to be on",
+ "just_turned_off: '%s' is still on after %s seconds, assuming it was intended to be on",
entity_id,
total_sleep,
)
From 5fa74a838fbb08b5e21038601d2c617886c08d62 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 28 Jul 2023 16:55:46 -0700
Subject: [PATCH 071/549] Typing fixes (#674)
* Typing fixes
* Ignore tyoe
---
custom_components/adaptive_lighting/switch.py | 69 ++++++++++---------
1 file changed, 38 insertions(+), 31 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 129f7626..6b463bc9 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -12,7 +12,7 @@ import math
from copy import deepcopy
from dataclasses import dataclass
from datetime import timedelta
-from typing import TYPE_CHECKING, Any, Literal
+from typing import TYPE_CHECKING, Any, Literal, cast
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
@@ -64,6 +64,7 @@ from homeassistant.const import (
SUN_EVENT_SUNSET,
)
from homeassistant.core import (
+ CALLBACK_TYPE,
Context,
Event,
HomeAssistant,
@@ -362,6 +363,7 @@ def _switches_from_service_call(
ent_reg = entity_registry.async_get(hass)
for entity_id in switch_entity_ids:
ent_entry = ent_reg.async_get(entity_id)
+ assert ent_entry is not None
config_id = ent_entry.config_entry_id
switches.append(hass.data[DOMAIN][config_id]["instance"])
return switches
@@ -418,18 +420,16 @@ def _fire_manual_control_event(
switch: AdaptiveSwitch,
light: str,
context: Context,
- is_async: bool = True,
):
"""Fire an event that 'light' is marked as manual_control."""
hass = switch.hass
- fire = hass.bus.async_fire if is_async else hass.bus.fire
_LOGGER.debug(
"'adaptive_lighting.manual_control' event fired for %s for light %s",
switch.entity_id,
light,
)
switch.manager.mark_as_manual_control(light)
- fire(
+ hass.bus.async_fire(
f"{DOMAIN}.manual_control",
{ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id},
context=context,
@@ -527,16 +527,17 @@ async def async_setup_entry( # noqa: PLR0915
switch.manager.lights.update(all_lights)
for light in all_lights:
if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light):
+ context = switch.create_context(
+ "service",
+ parent=service_call.context,
+ )
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],
- context=switch.create_context(
- "service",
- parent=service_call.context,
- ),
+ context=context,
+ transition=data[CONF_TRANSITION],
+ adapt_brightness=data[ATTR_ADAPT_BRIGHTNESS],
+ adapt_color=data[ATTR_ADAPT_COLOR],
+ prefer_rgb_color=data[CONF_PREFER_RGB_COLOR],
)
@callback
@@ -603,7 +604,7 @@ async def async_setup_entry( # noqa: PLR0915
def validate(
- config_entry: ConfigEntry,
+ config_entry: ConfigEntry | None,
service_data: dict[str, Any] | None = None,
defaults: dict[str, Any] | None = None,
) -> dict[str, Any]:
@@ -662,7 +663,9 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
@bind_hass
def _supported_features(hass: HomeAssistant, light: str) -> set[str]:
state = hass.states.get(light)
+ assert state is not None
supported_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ assert isinstance(supported_features, int)
supported = {
key for key, value in _SUPPORT_OPTS.items() if supported_features & value
}
@@ -739,7 +742,7 @@ def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]:
def _add_missing_attributes(
old_attributes: dict[str, Any],
new_attributes: dict[str, Any],
-) -> dict[str, Any]:
+) -> tuple[dict[str, Any], dict[str, Any]]:
if not any(
attr in old_attributes and attr in new_attributes
for attr in [ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR]
@@ -854,7 +857,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# Set other attributes
self._icon = ICON_MAIN
- self._state = None
+ self._state: bool | None = None
# Tracks 'on' → 'off' state changes
self._on_to_off_event: dict[str, Event] = {}
@@ -869,8 +872,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._settings: dict[str, Any] = {}
# Set and unset tracker in async_turn_on and async_turn_off
- self.remove_listeners = []
- self.remove_interval: Callable[[], None] = lambda: None
+ self.remove_listeners: list[CALLBACK_TYPE] = []
+ self.remove_interval: CALLBACK_TYPE = lambda: None
_LOGGER.debug(
"%s: Setting up with '%s',"
" config_entry.data: '%s',"
@@ -950,7 +953,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
sunset_offset=data[CONF_SUNSET_OFFSET],
sunset_time=data[CONF_SUNSET_TIME],
min_sunset_time=data[CONF_MIN_SUNSET_TIME],
- time_zone=self.hass.config.time_zone,
transition=data[CONF_TRANSITION],
)
_LOGGER.debug(
@@ -984,9 +986,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
EVENT_HOMEASSISTANT_STARTED,
self._setup_listeners,
)
- last_state = await self.async_get_last_state()
+ last_state: State | None = await self.async_get_last_state()
is_new_entry = last_state is None # newly added to HA
- if is_new_entry or last_state.state == STATE_ON:
+ if is_new_entry or last_state.state == STATE_ON: # type: ignore[union-attr]
await self.async_turn_on(adapt_lights=not self._only_once)
else:
self._state = False
@@ -1095,7 +1097,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the attributes of the switch."""
- extra_state_attributes = {"configuration": self._config}
+ extra_state_attributes: dict[str, Any] = {"configuration": self._config}
if not self.is_on:
for key in self._settings:
extra_state_attributes[key] = None
@@ -1218,7 +1220,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and not (self._settings["force_rgb_color"] and "color" in features)
):
_LOGGER.debug("%s: Setting color_temp of light %s", self._name, light)
- attributes = self.hass.states.get(light).attributes
+ state = self.hass.states.get(light)
+ assert isinstance(state, State)
+ attributes = state.attributes
min_kelvin = attributes["min_color_temp_kelvin"]
max_kelvin = attributes["max_color_temp_kelvin"]
color_temp_kelvin = self._settings["color_temp_kelvin"]
@@ -1257,11 +1261,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
async def _adapt_light(
self,
light: str,
+ context: Context,
transition: int | None = None,
adapt_brightness: bool | None = None,
adapt_color: bool | None = None,
prefer_rgb_color: bool | None = None,
- context: Context | None = None,
) -> None:
if (lock := self._locks.get(light)) is not None and lock.locked():
_LOGGER.debug("%s: '%s' is locked", self._name, light)
@@ -1358,7 +1362,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
lights: list[str] | None = None,
transition: int | None = None,
force: bool = False,
- context: Context | None = None,
+ context: Context = None,
) -> None:
assert context is not None
_LOGGER.debug(
@@ -1406,6 +1410,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
return
adapt_brightness = self.adapt_brightness_switch.is_on
adapt_color = self.adapt_color_switch.is_on
+ assert isinstance(adapt_brightness, bool)
+ assert isinstance(adapt_color, bool)
for light in filtered_lights:
if not is_on(self.hass, light):
@@ -1450,7 +1456,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
transition,
context.id,
)
- await self._adapt_light(light, transition, context=context)
+ await self._adapt_light(light, context, transition)
async def _sleep_mode_switch_state_event_action(self, event: Event) -> None:
if not _is_state_event(event, (STATE_ON, STATE_OFF)):
@@ -1581,7 +1587,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
self.hass = hass
data = validate(config_entry)
self._icon = icon
- self._state = None
+ self._state: bool | None = None
self._which = which
name = data[CONF_NAME]
self._unique_id = f"{name}_{slugify(self._which)}"
@@ -1653,7 +1659,7 @@ def lerp_color_hsv(
# Convert back to RGB
rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv))
assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}"
- return rgb
+ return cast(tuple[int, int, int], rgb)
@dataclass(frozen=True)
@@ -1677,7 +1683,6 @@ class SunLightSettings:
sunset_offset: datetime.timedelta | None
sunset_time: datetime.time | None
min_sunset_time: datetime.time | None
- time_zone: datetime.tzinfo
transition: int
def get_sun_events(self, date: datetime.datetime) -> list[tuple[str, float]]:
@@ -1809,7 +1814,8 @@ class SunLightSettings:
delta = abs(self.min_color_temp - self.sleep_color_temp)
ct = (delta * abs(1 + percent)) + self.sleep_color_temp
return 5 * round(ct / 5) # round to nearest 5
- return None
+ msg = "Should not happen"
+ raise ValueError(msg)
def get_settings(
self,
@@ -2397,7 +2403,7 @@ class AdaptiveLightingManager:
last_service_data = self.last_service_data.get(light)
if last_service_data is None:
- return None
+ return False
compare_to = functools.partial(
_attributes_have_changed,
light=light,
@@ -2411,6 +2417,7 @@ class AdaptiveLightingManager:
# can happen e.g. using zigbee2mqtt with 'report: false' in device settings.
await self.hass.helpers.entity_component.async_update_entity(light)
refreshed_state = self.hass.states.get(light)
+ assert refreshed_state is not None
changed = compare_to(
old_attributes=last_service_data,
@@ -2572,7 +2579,7 @@ class _AsyncSingleShotTimer:
self.delay = delay
self.callback = callback
self.task = None
- self.start_time: int | None = None
+ self.start_time: datetime.datetime | None = None
async def _run(self):
"""Run the timer. Don't call this directly, use start() instead."""
From 5c897f76da59169728132289852f9ffeaa6f4bc1 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 28 Jul 2023 17:17:26 -0700
Subject: [PATCH 072/549] More typing fixes (#675)
---
custom_components/adaptive_lighting/switch.py | 44 ++++++++++---------
tests/test_switch.py | 14 +++---
2 files changed, 32 insertions(+), 26 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 6b463bc9..9f2c11e3 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -408,10 +408,10 @@ async def handle_change_switch_settings(
switch.manager.reset(*switch.lights, reset_manual_control=False)
if switch.is_on:
await switch._update_attrs_and_maybe_adapt_lights( # pylint: disable=protected-access
- switch.lights,
+ context=switch.create_context("service", parent=service_call.context),
+ lights=switch.lights,
transition=switch.initial_transition,
force=True,
- context=switch.create_context("service", parent=service_call.context),
)
@@ -561,15 +561,16 @@ async def async_setup_entry( # noqa: PLR0915
else:
switch.manager.reset(*all_lights)
if switch.is_on:
+ context = switch.create_context(
+ "service",
+ parent=service_call.context,
+ )
# pylint: disable=protected-access
await switch._update_attrs_and_maybe_adapt_lights(
- all_lights,
+ context=context,
+ lights=all_lights,
transition=switch.initial_transition,
force=True,
- context=switch.create_context(
- "service",
- parent=service_call.context,
- ),
)
# Register `apply` service
@@ -1124,7 +1125,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._context_cnt += 1
return context
- async def async_turn_on( # pylint: disable=arguments-differ
+ async def async_turn_on( # type: ignore[override]
self,
adapt_lights: bool = True,
) -> None:
@@ -1141,9 +1142,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
await self._setup_listeners()
if adapt_lights:
await self._update_attrs_and_maybe_adapt_lights(
+ context=self.create_context("turn_on"),
transition=self.initial_transition,
force=True,
- context=self.create_context("turn_on"),
)
async def async_turn_off(self, **kwargs) -> None: # noqa: ARG002
@@ -1156,9 +1157,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
async def _async_update_at_interval_action(self, now=None) -> None: # noqa: ARG002
await self._update_attrs_and_maybe_adapt_lights(
+ context=self.create_context("interval"),
transition=self._transition,
force=False,
- context=self.create_context("interval"),
)
async def prepare_adaptation_data(
@@ -1271,7 +1272,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
_LOGGER.debug("%s: '%s' is locked", self._name, light)
return
- if self.manager.is_proactively_adapting(context.parent_id):
+ if context.parent_id is not None and self.manager.is_proactively_adapting(
+ context.parent_id,
+ ):
# Skip if adaptation was already executed by the service call interceptor
_LOGGER.debug(
"%s: Skipping reactive adaptation of %s",
@@ -1359,10 +1362,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912
self,
+ *,
+ context: Context,
lights: list[str] | None = None,
transition: int | None = None,
force: bool = False,
- context: Context = None,
) -> None:
assert context is not None
_LOGGER.debug(
@@ -1470,15 +1474,15 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# Reset the manually controlled status when the "sleep mode" changes
self.manager.reset(*self.lights)
await self._update_attrs_and_maybe_adapt_lights(
+ context=self.create_context("sleep", parent=event.context),
transition=self._sleep_transition,
force=True,
- context=self.create_context("sleep", parent=event.context),
)
async def _light_state_event_action(self, event: Event) -> None:
old_state = event.data.get("old_state")
new_state = event.data.get("new_state")
- entity_id = event.data.get("entity_id")
+ entity_id: str = event.data["entity_id"]
if old_state is None or new_state is None:
return
@@ -1565,10 +1569,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
await self._update_attrs_and_maybe_adapt_lights(
+ context=self.create_context("light_event", parent=event.context),
lights=[entity_id],
transition=self.initial_transition,
force=True,
- context=self.create_context("light_event", parent=event.context),
)
@@ -1637,8 +1641,8 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
def lerp_color_hsv(
- rgb1: tuple[int, int, int],
- rgb2: tuple[int, int, int],
+ rgb1: tuple[float, float, float],
+ rgb2: tuple[float, float, float],
t: float,
) -> tuple[int, int, int]:
"""Linearly interpolate between two RGB colors in HSV color space."""
@@ -2163,10 +2167,10 @@ class AdaptiveLightingManager:
if not switch.is_on:
continue
await switch._update_attrs_and_maybe_adapt_lights(
- [light],
+ context=switch.create_context("autoreset"),
+ lights=[light],
transition=switch.initial_transition,
force=True,
- context=switch.create_context("autoreset"),
)
_LOGGER.debug(
"Auto resetting 'manual_control' status of '%s' because"
@@ -2289,7 +2293,7 @@ class AdaptiveLightingManager:
if (
timer is not None
and timer.is_running()
- and event.time_fired > timer.start_time
+ and event.time_fired > timer.start_time # type: ignore[operator]
):
# Restart the auto reset timer
timer.start()
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 328e493f..0838a843 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -479,7 +479,7 @@ async def test_light_settings(hass):
async def patch_time_and_get_updated_states(time):
with patch("homeassistant.util.dt.utcnow", return_value=time):
await switch._update_attrs_and_maybe_adapt_lights(
- transition=0, context=context, force=True
+ context=context, transition=0, force=True
)
await hass.async_block_till_done()
return [hass.states.get(light) for light in lights]
@@ -558,7 +558,7 @@ async def test_manual_control(hass):
manual_control = switch.manager.manual_control
async def update():
- await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context)
+ await switch._update_attrs_and_maybe_adapt_lights(context=context, transition=0)
await hass.async_block_till_done()
async def turn_light(state, **kwargs):
@@ -710,7 +710,7 @@ async def test_auto_reset_manual_control(hass):
manual_control = switch.manager.manual_control
async def update():
- await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context)
+ await switch._update_attrs_and_maybe_adapt_lights(context=context, transition=0)
await hass.async_block_till_done()
async def turn_light(state, **kwargs):
@@ -834,7 +834,7 @@ async def test_switch_off_on_off(hass):
async def update():
await switch._update_attrs_and_maybe_adapt_lights(
- transition=0, context=switch.create_context("test")
+ context=switch.create_context("test"), transition=0
)
await hass.async_block_till_done()
@@ -982,7 +982,9 @@ async def test_state_change_handlers(hass):
async def update(force: bool = False):
await switch._update_attrs_and_maybe_adapt_lights(
- force=force, transition=0, context=context
+ context=context,
+ force=force,
+ transition=0,
)
await hass.async_block_till_done()
@@ -1005,7 +1007,7 @@ async def test_state_change_handlers(hass):
# 2 Adapt from sleep with a 'transition'.
await switch.sleep_mode_switch.async_turn_off()
await switch._update_attrs_and_maybe_adapt_lights(
- force=False, transition=0, context=context
+ context=context, force=False, transition=0
)
await hass.async_block_till_done()
current_service_data = switch.manager.last_service_data
From f28c0315513c080355641b3cd334a46dd9b750d6 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 28 Jul 2023 17:20:01 -0700
Subject: [PATCH 073/549] Fix alias in FUNDING.yml (#677)
---
.github/FUNDING.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index e6701aec..8bb28be4 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1 @@
-github: [basnijholz, RubenKelevra]
+github: [basnijholt]
From 8967747f31345fdbf1f6f274fc8b434e9f2e572d Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 28 Jul 2023 17:25:50 -0700
Subject: [PATCH 074/549] =?UTF-8?q?Release=201.18.0=20(fixes=20lights=20ac?=
=?UTF-8?q?cidentally=20turning=20on=20=F0=9F=9A=80=F0=9F=8E=89)=20(#676)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 8695c555..34045b68 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.17.5"
+ "version": "1.18.0"
}
From 8322e516ba260cb0aca040f186b9ee43a5168ab6 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 28 Jul 2023 18:41:41 -0700
Subject: [PATCH 075/549] Add link to repo in README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d1d7aa16..8721fc42 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@

-Adaptive Lighting is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control.
+[Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control.
Download and install directly through [HACS (Home Assistant Community Store)](https://hacs.xyz/)
From 68985199f6ec6fb0f5a338534298e83a0a8f6f2f Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 29 Jul 2023 21:48:45 -0700
Subject: [PATCH 076/549] Rename lights in tests (#681)
* Rename lights in tests
* Remove unused dependencies
---
tests/test_switch.py | 194 ++++++++++++++++++-------------------------
1 file changed, 82 insertions(+), 112 deletions(-)
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 0838a843..be0c7d37 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -118,8 +118,9 @@ LAT_LONG_TZS = [
(32.87336, -117.22743, "US/Pacific"),
]
-ENTITY_LIGHT = "light.bed_light"
-ENTITY_LIGHT3 = "light.kitchen_lights"
+ENTITY_LIGHT_1 = "light.light_1"
+ENTITY_LIGHT_2 = "light.light_2"
+ENTITY_LIGHT_3 = "light.light_3"
_SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}"
ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}"
ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}"
@@ -128,11 +129,6 @@ ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}"
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
-GLOBAL_TEST_DEPENDENCIES = [
- "test_adaptive_lighting_switches",
- "test_light_settings",
-]
-
def create_random_context() -> str:
return Context(id=ulid_transform.ulid_now(), parent_id=None)
@@ -173,8 +169,8 @@ async def setup_lights(hass: HomeAssistant):
{
"platform": "template",
"lights": {
- "bed_light": {
- "friendly_name": "Bed Light",
+ "light_1": {
+ "friendly_name": "light_1",
"unique_id": "light_1",
"turn_on": None,
"turn_off": None,
@@ -182,8 +178,8 @@ async def setup_lights(hass: HomeAssistant):
"set_temperature": None,
"set_color": None,
},
- "ceiling_lights": {
- "friendly_name": "Ceiling Lights",
+ "light_2": {
+ "friendly_name": "light_2",
"unique_id": "light_2",
"turn_on": None,
"turn_off": None,
@@ -191,8 +187,8 @@ async def setup_lights(hass: HomeAssistant):
"set_temperature": None,
"set_color": None,
},
- "kitchen_lights": {
- "friendly_name": "Kitchen Lights",
+ "light_3": {
+ "friendly_name": "light_3",
"unique_id": "light_3",
"turn_on": None,
"turn_off": None,
@@ -229,18 +225,18 @@ async def setup_lights_and_switch(hass, extra_conf=None, all_lights: bool = Fals
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: ENTITY_LIGHT},
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_1},
blocking=True,
)
# Setup switch
lights = [
- ENTITY_LIGHT,
- "light.ceiling_lights",
+ ENTITY_LIGHT_1,
+ ENTITY_LIGHT_2,
]
if all_lights:
- lights.append(ENTITY_LIGHT3)
+ lights.append(ENTITY_LIGHT_3)
assert all(hass.states.get(light) is not None for light in lights)
_, switch = await setup_switch(
@@ -341,7 +337,6 @@ async def test_adaptive_lighting_switches(hass):
@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS)
-@pytest.mark.dependency("test_adaptive_lighting_switches")
async def test_adaptive_lighting_time_zones_with_default_settings(
hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name
):
@@ -530,11 +525,10 @@ async def test_light_settings(hass):
assert_expected_color_temp(state)
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_manager_not_tracking_untracked_lights(hass):
"""Test that lights that are not in a Adaptive Lighting switch aren't tracked."""
switch, _ = await setup_lights_and_switch(hass)
- light = "light.kitchen_lights"
+ light = ENTITY_LIGHT_3
assert light not in switch.lights
for state in [True, False]:
await hass.services.async_call(
@@ -550,7 +544,6 @@ async def test_manager_not_tracking_untracked_lights(hass):
assert light not in switch.manager.lights
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_manual_control(hass):
"""Test the 'manual control' tracking."""
switch, (light, *_) = await setup_lights_and_switch(hass)
@@ -565,7 +558,7 @@ async def test_manual_control(hass):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON if state else SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs},
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs},
blocking=True,
)
await hass.async_block_till_done()
@@ -583,7 +576,7 @@ async def test_manual_control(hass):
async def change_manual_control(set_to, extra_service_data=None):
if extra_service_data is None:
- extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT]}
+ extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT_1]}
await hass.services.async_call(
DOMAIN,
SERVICE_SET_MANUAL_CONTROL,
@@ -608,48 +601,48 @@ async def test_manual_control(hass):
# Nothing is manually controlled
await update()
- assert not manual_control[ENTITY_LIGHT]
- # Call light.turn_on for ENTITY_LIGHT
+ assert not manual_control[ENTITY_LIGHT_1]
+ # Call light.turn_on for ENTITY_LIGHT_1
await turn_light(True, brightness=increased_brightness())
- # Check that ENTITY_LIGHT is manually controlled
- assert manual_control[ENTITY_LIGHT]
+ # Check that ENTITY_LIGHT_1 is manually controlled
+ assert manual_control[ENTITY_LIGHT_1]
# Test adaptive_lighting.set_manual_control
await change_manual_control(False)
- # Check that ENTITY_LIGHT is not manually controlled
- assert not manual_control[ENTITY_LIGHT]
+ # Check that ENTITY_LIGHT_1 is not manually controlled
+ assert not manual_control[ENTITY_LIGHT_1]
# Check that toggling light off to on resets manual control
await change_manual_control(True)
- assert manual_control[ENTITY_LIGHT]
+ assert manual_control[ENTITY_LIGHT_1]
await turn_light(False)
await turn_light(True, brightness=increased_brightness())
- assert hass.states.get(ENTITY_LIGHT).state == STATE_ON
- assert not manual_control[ENTITY_LIGHT], manual_control
+ assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
+ assert not manual_control[ENTITY_LIGHT_1], manual_control
# Check that toggling (sleep mode) switch resets manual control
for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]:
await change_manual_control(True)
- assert manual_control[ENTITY_LIGHT]
+ assert manual_control[ENTITY_LIGHT_1]
await turn_switch(False, entity_id)
await turn_switch(True, entity_id)
- assert not manual_control[ENTITY_LIGHT]
+ assert not manual_control[ENTITY_LIGHT_1]
# Check that manual control is still enabled if set while bulb is off.
# Test issue #37
await turn_light(False)
await change_manual_control(True)
await turn_light(True)
- assert manual_control[ENTITY_LIGHT]
+ assert manual_control[ENTITY_LIGHT_1]
# Check that when 'adapt_brightness' is off, changing the brightness
# doesn't mark it as manually controlled but changing color_temp
# does
await turn_light(False)
await turn_light(True) # reset manually controlled status
- assert not manual_control[ENTITY_LIGHT]
+ assert not manual_control[ENTITY_LIGHT_1]
await switch.adapt_brightness_switch.async_turn_off()
await turn_light(True, brightness=increased_brightness())
- assert not manual_control[ENTITY_LIGHT]
+ assert not manual_control[ENTITY_LIGHT_1]
mired_range = (light.min_color_temp_kelvin, light.max_color_temp_kelvin)
kelvin_range = (
color_temperature_mired_to_kelvin(mired_range[1]),
@@ -659,7 +652,7 @@ async def test_manual_control(hass):
await turn_light(
True, color_temp_kelvin=(light._attr_color_temp + 100) % ptp_kelvin
)
- assert manual_control[ENTITY_LIGHT]
+ assert manual_control[ENTITY_LIGHT_1]
await switch.adapt_brightness_switch.async_turn_on() # turn on again
# Check that when 'adapt_color' is off, changing the color
@@ -667,12 +660,12 @@ async def test_manual_control(hass):
# does
await turn_light(False) # reset manually controlled status
await turn_light(True)
- assert not manual_control[ENTITY_LIGHT]
+ assert not manual_control[ENTITY_LIGHT_1]
await switch.adapt_color_switch.async_turn_off()
await turn_light(True, color_temp=increased_color_temp())
- assert not manual_control[ENTITY_LIGHT]
+ assert not manual_control[ENTITY_LIGHT_1]
await turn_light(True, brightness=increased_brightness())
- assert manual_control[ENTITY_LIGHT]
+ assert manual_control[ENTITY_LIGHT_1]
# Check that when 'adapt_color' adapt_brightness are both off
# nothing marks it as manually controlled
@@ -680,7 +673,7 @@ async def test_manual_control(hass):
await turn_light(True)
await switch.adapt_color_switch.async_turn_off()
await switch.adapt_brightness_switch.async_turn_off()
- assert not manual_control[ENTITY_LIGHT]
+ assert not manual_control[ENTITY_LIGHT_1]
await turn_light(True, color_temp=increased_color_temp())
await turn_light(True, brightness=increased_brightness())
await turn_light(
@@ -688,7 +681,7 @@ async def test_manual_control(hass):
color_temp=increased_color_temp(),
brightness=increased_brightness(),
)
- assert not manual_control[ENTITY_LIGHT]
+ assert not manual_control[ENTITY_LIGHT_1]
# Turn switches on again
await switch.adapt_color_switch.async_turn_on()
await switch.adapt_brightness_switch.async_turn_on()
@@ -701,7 +694,6 @@ async def test_manual_control(hass):
assert all([not manual_control[eid] for eid in switch.lights])
-@pytest.mark.dependency(depends=[*GLOBAL_TEST_DEPENDENCIES, "test_manual_control"])
async def test_auto_reset_manual_control(hass):
switch, (light, *_) = await setup_lights_and_switch(
hass, {CONF_AUTORESET_CONTROL: 0.1}
@@ -752,7 +744,6 @@ async def test_auto_reset_manual_control(hass):
assert not manual_control[light.entity_id]
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_apply_service(hass):
"""Test adaptive_lighting.apply service."""
switch, (_, _, light) = await setup_lights_and_switch(hass)
@@ -817,9 +808,6 @@ async def test_apply_service(hass):
assert old_state[ATTR_COLOR_TEMP_KELVIN] == new_state[ATTR_COLOR_TEMP_KELVIN]
-@pytest.mark.dependency(
- depends=[*GLOBAL_TEST_DEPENDENCIES, "test_apply_service", "test_manual_control"]
-)
async def test_switch_off_on_off(hass):
"""Test switch rapid off_on_off."""
@@ -827,7 +815,7 @@ async def test_switch_off_on_off(hass):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON if state else SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs},
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs},
blocking=True,
)
await hass.async_block_till_done()
@@ -846,23 +834,23 @@ async def test_switch_off_on_off(hass):
# Turn light off with transition
await turn_light(False, transition=1)
- assert not switch.manager.manual_control[ENTITY_LIGHT]
+ assert not switch.manager.manual_control[ENTITY_LIGHT_1]
# Set state to on after a second (like happens IRL)
await asyncio.sleep(1e-3)
- hass.states.async_set(ENTITY_LIGHT, STATE_ON)
+ hass.states.async_set(ENTITY_LIGHT_1, STATE_ON)
# Set state to off after a second (like happens IRL)
await asyncio.sleep(1e-3)
- hass.states.async_set(ENTITY_LIGHT, STATE_OFF)
+ hass.states.async_set(ENTITY_LIGHT_1, STATE_OFF)
# Now we test whether the sleep task is there
- assert ENTITY_LIGHT in switch.manager.sleep_tasks
- sleep_task = switch.manager.sleep_tasks[ENTITY_LIGHT]
+ assert ENTITY_LIGHT_1 in switch.manager.sleep_tasks
+ sleep_task = switch.manager.sleep_tasks[ENTITY_LIGHT_1]
assert not sleep_task.cancelled()
# A 'light.turn_on' event should cancel that task
await turn_light(turn_light_state_at_end)
await update()
- state = hass.states.get(ENTITY_LIGHT).state
+ state = hass.states.get(ENTITY_LIGHT_1).state
if turn_light_state_at_end:
assert sleep_task.cancelled()
assert state == STATE_ON
@@ -870,7 +858,6 @@ async def test_switch_off_on_off(hass):
assert state == STATE_OFF
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
def test_color_difference_redmean():
"""Test color_difference_redmean function."""
for _ in range(10):
@@ -933,7 +920,6 @@ def test_attributes_have_changed():
)
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_state_change_handlers(hass):
"""
Test AdaptiveLightingManager's EVENT_STATE_CHANGED listener.
@@ -955,7 +941,7 @@ async def test_state_change_handlers(hass):
async def set_brightness(val: int):
# 'Unsafe' set but we know what we're doing.
hass.states.async_set(
- ENTITY_LIGHT, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1}
+ ENTITY_LIGHT_1, "on", {ATTR_BRIGHTNESS: val, ATTR_SUPPORTED_FEATURES: 1}
)
await hass.async_block_till_done()
# Call code in AdaptiveLightingManager
@@ -963,7 +949,7 @@ async def test_state_change_handlers(hass):
EVENT_STATE_CHANGED,
{
"new_state": {
- ATTR_ENTITY_ID: ENTITY_LIGHT,
+ ATTR_ENTITY_ID: ENTITY_LIGHT_1,
"state": "on",
ATTR_BRIGHTNESS: val,
}
@@ -975,7 +961,7 @@ async def test_state_change_handlers(hass):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON if state else SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs},
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs},
blocking=True,
)
await hass.async_block_till_done()
@@ -998,11 +984,11 @@ async def test_state_change_handlers(hass):
blocking=True,
)
await hass.async_block_till_done()
- assert switch.manager.last_state_change.get(ENTITY_LIGHT)
- assert len(switch.manager.last_state_change[ENTITY_LIGHT]) == 1
- assert not switch.manager.transition_timers.get(ENTITY_LIGHT)
+ assert switch.manager.last_state_change.get(ENTITY_LIGHT_1)
+ assert len(switch.manager.last_state_change[ENTITY_LIGHT_1]) == 1
+ assert not switch.manager.transition_timers.get(ENTITY_LIGHT_1)
last_service_data = deepcopy(switch.manager.last_service_data)
- assert last_service_data.get(ENTITY_LIGHT)
+ assert last_service_data.get(ENTITY_LIGHT_1)
# 2 Adapt from sleep with a 'transition'.
await switch.sleep_mode_switch.async_turn_off()
@@ -1068,9 +1054,9 @@ async def test_state_change_handlers(hass):
# asyncio.sleep(3)
# 4. Assert the transition timer started and everything was filled.
listener = switch.manager
- assert listener.last_state_change.get(ENTITY_LIGHT)
- assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events
- assert listener.transition_timers.get(ENTITY_LIGHT)
+ assert listener.last_state_change.get(ENTITY_LIGHT_1)
+ assert len(listener.last_state_change[ENTITY_LIGHT_1]) == total_events
+ assert listener.transition_timers.get(ENTITY_LIGHT_1)
# 5. Execute some checks during a transition
_LOGGER.debug("Test detect_non_ha_changes:")
@@ -1080,25 +1066,25 @@ async def test_state_change_handlers(hass):
assert switch._detect_non_ha_changes
await asyncio.sleep(transition_used / 3)
# Ensure the timer still exists
- timer = listener.transition_timers.get(ENTITY_LIGHT)
+ timer = listener.transition_timers.get(ENTITY_LIGHT_1)
assert timer and timer.is_running()
last_service_data = deepcopy(current_service_data)
await update()
- assert not switch.manager.manual_control[ENTITY_LIGHT]
+ assert not switch.manager.manual_control[ENTITY_LIGHT_1]
await update()
- assert not switch.manager.manual_control[ENTITY_LIGHT]
- timer = listener.transition_timers.get(ENTITY_LIGHT)
+ assert not switch.manager.manual_control[ENTITY_LIGHT_1]
+ timer = listener.transition_timers.get(ENTITY_LIGHT_1)
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.
await asyncio.sleep(transition_used)
- assert listener.last_state_change.get(ENTITY_LIGHT)
- assert len(listener.last_state_change[ENTITY_LIGHT]) == total_events
+ assert listener.last_state_change.get(ENTITY_LIGHT_1)
+ assert len(listener.last_state_change[ENTITY_LIGHT_1]) == 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)
+ timer = listener.transition_timers.get(ENTITY_LIGHT_1)
assert not timer or not timer.is_running()
# build last service data
@@ -1108,35 +1094,25 @@ async def test_state_change_handlers(hass):
await turn_light(True, brightness=40)
await turn_light(True, brightness=20)
await update(force=False)
- assert switch.manager.manual_control[ENTITY_LIGHT]
+ assert switch.manager.manual_control[ENTITY_LIGHT_1]
await update(force=True)
- assert switch.manager.manual_control[ENTITY_LIGHT]
+ assert switch.manager.manual_control[ENTITY_LIGHT_1]
# turn light off then on should reset manual control.
await turn_light(False)
await turn_light(True)
- assert not switch.manager.manual_control[ENTITY_LIGHT]
+ assert not switch.manager.manual_control[ENTITY_LIGHT_1]
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
+ # On next update ENTITY_LIGHT_1 should be marked as manually controlled
await update(force=False)
- assert switch.manager.last_service_data.get(ENTITY_LIGHT) is not None
- assert switch.manager.last_state_change.get(ENTITY_LIGHT) is not None
- assert switch.manager.manual_control[ENTITY_LIGHT]
+ assert switch.manager.last_service_data.get(ENTITY_LIGHT_1) is not None
+ assert switch.manager.last_state_change.get(ENTITY_LIGHT_1) is not None
+ assert switch.manager.manual_control[ENTITY_LIGHT_1]
-@pytest.mark.dependency(
- depends=[
- *GLOBAL_TEST_DEPENDENCIES,
- "test_manual_control",
- "test_apply_service",
- "test_attributes_have_changed",
- "test_state_change_handling",
- ]
-)
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
def test_is_our_context():
"""Test is_our_context function."""
context = create_context(DOMAIN, "test", 0)
@@ -1211,7 +1187,6 @@ async def test_turn_on_and_off_when_already_at_that_state(hass):
await hass.async_block_till_done()
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_async_update_at_interval_action(hass):
"""Test '_async_update_at_interval_action' method."""
_, switch = await setup_switch(hass, {})
@@ -1219,7 +1194,6 @@ async def test_async_update_at_interval_action(hass):
@pytest.mark.parametrize("separate_turn_on_commands", (True, False))
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_separate_turn_on_commands(hass, separate_turn_on_commands):
"""Test 'separate_turn_on_commands' argument."""
switch, (light, *_) = await setup_lights_and_switch(
@@ -1256,7 +1230,6 @@ async def test_separate_turn_on_commands(hass, separate_turn_on_commands):
assert sleep_color_temp != color_temp
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_area(hass):
switch, (light, *_) = await setup_lights_and_switch(hass)
@@ -1293,7 +1266,6 @@ async def test_area(hass):
assert light.entity_id not in switch.manager.last_service_data
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_change_switch_settings_service(hass):
"""Test adaptive_lighting.change_switch_settings service."""
switch, (_, _, light) = await setup_lights_and_switch(hass)
@@ -1346,7 +1318,6 @@ async def test_change_switch_settings_service(hass):
assert switch._sun_light_settings.min_color_temp == 2500
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_cancellable_service_calls_task(hass):
"""Test the creation and execution of the task that wraps adaptation service calls."""
(light, *_) = await setup_lights(hass)
@@ -1377,7 +1348,6 @@ async def test_cancellable_service_calls_task(hass):
assert task.done()
-@pytest.mark.dependency(depends=GLOBAL_TEST_DEPENDENCIES)
async def test_service_calls_task_cancellation(hass):
"""Tests if the task that wraps ongoing adaptation service calls gets cancelled."""
_, switch = await setup_switch(hass, {})
@@ -1441,7 +1411,7 @@ async def test_proactive_adaptation(hass):
)
event_context_ids = await _turn_on_and_track_event_contexts(
- hass, "test_context", ENTITY_LIGHT3
+ hass, "test_context", ENTITY_LIGHT_3
)
# Expect a single service call
@@ -1449,7 +1419,7 @@ async def test_proactive_adaptation(hass):
assert event_context_ids == ["test_context"]
# Expect adapted light state
- state = hass.states.get(ENTITY_LIGHT3)
+ state = hass.states.get(ENTITY_LIGHT_3)
# Sun light settings use %, state only contains absolute
assert state.attributes[ATTR_BRIGHTNESS] == 171 # == 67%
assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448
@@ -1476,7 +1446,7 @@ async def test_proactive_adaptation_with_separate_commands(hass):
)
event_context_ids = await _turn_on_and_track_event_contexts(
- hass, "test_context", ENTITY_LIGHT3
+ hass, "test_context", ENTITY_LIGHT_3
)
# Expect two service calls
@@ -1485,7 +1455,7 @@ async def test_proactive_adaptation_with_separate_commands(hass):
assert is_our_context_id(event_context_ids[1])
# Expect adapted light state
- state = hass.states.get(ENTITY_LIGHT3)
+ state = hass.states.get(ENTITY_LIGHT_3)
assert state.attributes[ATTR_BRIGHTNESS] == 171
assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == 3448
@@ -1504,7 +1474,7 @@ async def test_proactive_adaptation_toggle(hass):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TOGGLE,
- {ATTR_ENTITY_ID: ENTITY_LIGHT3},
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_3},
blocking=True,
context=Context(id="test1"),
)
@@ -1515,7 +1485,7 @@ async def test_proactive_adaptation_toggle(hass):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TOGGLE,
- {ATTR_ENTITY_ID: ENTITY_LIGHT3},
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_3},
blocking=True,
context=Context(id="test2"),
)
@@ -1540,14 +1510,14 @@ async def test_proactive_adaptation_transition_override(hass):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: ENTITY_LIGHT3},
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_3},
blocking=True,
)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
- {ATTR_ENTITY_ID: ENTITY_LIGHT3, ATTR_TRANSITION: 456},
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_3, ATTR_TRANSITION: 456},
blocking=True,
)
@@ -1560,7 +1530,7 @@ async def test_proactive_adaptation_transition_override(hass):
assert set({ATTR_TRANSITION: 456}.items()).issubset(kwargs.items())
# Cleanup
- switch.manager.cancel_ongoing_adaptation_calls(ENTITY_LIGHT3)
+ switch.manager.cancel_ongoing_adaptation_calls(ENTITY_LIGHT_3)
async def test_two_switches_for_single_light(hass):
@@ -1588,7 +1558,7 @@ async def test_two_switches_for_single_light(hass):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON if state else SERVICE_TURN_OFF,
- {ATTR_ENTITY_ID: ENTITY_LIGHT, **kwargs},
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs},
blocking=True,
)
await hass.async_block_till_done()
@@ -1672,31 +1642,31 @@ async def test_adapt_until_sleep_and_rgb_colors(hass):
assert not switch._settings["force_rgb_color"]
assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
assert switch._settings["color_temp_kelvin"] > min_color_temp
- assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT]
+ assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT_1]
# One hour after sunset the brightness should be down and use RGB
await patch_time_and_update(after_sunset)
assert switch._settings["force_rgb_color"]
assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS
- assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT]
+ assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT_1]
# At sunrise the brightness should be max and use Kelvin
await patch_time_and_update(sunrise)
assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
assert switch._settings["color_temp_kelvin"] == min_color_temp
- assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT]
+ assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT_1]
# One hour before sunrise the brightness should smaller than max
# and use RGB
await patch_time_and_update(before_sunrise)
assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS
- assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT]
+ assert "rgb_color" in switch.manager.last_service_data[ENTITY_LIGHT_1]
# One hour after sunrise the brightness should be up and it should use Kelvin
await patch_time_and_update(after_sunrise)
assert switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
assert switch._settings["color_temp_kelvin"] > min_color_temp
- assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT]
+ assert "color_temp_kelvin" in switch.manager.last_service_data[ENTITY_LIGHT_1]
# Turn on sleep mode which make the brightness and color_temp
# deterministic regardless of the time
From 2b35ee1e5e8959a1315384536792f036b2ce6b2c Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 30 Jul 2023 11:49:40 -0700
Subject: [PATCH 077/549] Mark as manually controlled when using flash, effect,
or RGBW(W) (#684)
* Mark as manually controlled when using flash or effect
* Add comment
* Add RGBW and RGBWW
---
.../adaptive_lighting/adaptation_utils.py | 5 +++++
custom_components/adaptive_lighting/switch.py | 10 ++++++++--
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py
index 99d1eea5..e0ab6c47 100644
--- a/custom_components/adaptive_lighting/adaptation_utils.py
+++ b/custom_components/adaptive_lighting/adaptation_utils.py
@@ -13,6 +13,8 @@ from homeassistant.components.light import (
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ATTR_RGB_COLOR,
+ ATTR_RGBW_COLOR,
+ ATTR_RGBWW_COLOR,
ATTR_TRANSITION,
ATTR_XY_COLOR,
)
@@ -27,8 +29,11 @@ COLOR_ATTRS = { # Should ATTR_PROFILE be in here?
ATTR_HS_COLOR,
ATTR_RGB_COLOR,
ATTR_XY_COLOR,
+ ATTR_RGBW_COLOR,
+ ATTR_RGBWW_COLOR,
}
+
BRIGHTNESS_ATTRS = {
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_PCT,
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 9f2c11e3..181bd20d 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -21,6 +21,8 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
+ ATTR_EFFECT,
+ ATTR_FLASH,
ATTR_RGB_COLOR,
ATTR_SUPPORTED_COLOR_MODES,
ATTR_TRANSITION,
@@ -1450,6 +1452,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context.id,
)
else:
+ # Need to fire manual control event because of significant_change
_fire_manual_control_event(self, light, context)
else:
_LOGGER.debug(
@@ -2371,8 +2374,11 @@ class AdaptiveLightingManager:
and not force
):
keys = turn_on_event.data[ATTR_SERVICE_DATA].keys()
- if (adapt_color and COLOR_ATTRS.intersection(keys)) or (
- adapt_brightness and BRIGHTNESS_ATTRS.intersection(keys)
+ if (
+ (adapt_color and COLOR_ATTRS.intersection(keys))
+ or (adapt_brightness and BRIGHTNESS_ATTRS.intersection(keys))
+ or (ATTR_FLASH in keys)
+ or (ATTR_EFFECT in keys)
):
# Light was already on and 'light.turn_on' was not called by
# the adaptive_lighting integration.
From 2e25af3125544bb77720fcca270f61f01b6d87f8 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 30 Jul 2023 12:32:59 -0700
Subject: [PATCH 078/549] Do not intercept effect and flash calls (#685)
* Do not intercept effect and flash calls
* log
* Do not intercept flash
---
custom_components/adaptive_lighting/switch.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 181bd20d..58638c3d 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -2003,6 +2003,9 @@ class AdaptiveLightingManager:
if is_our_context(call.context):
return
+ if ATTR_EFFECT in data[CONF_PARAMS] or ATTR_FLASH in data[CONF_PARAMS]:
+ return
+
entity_ids = self._get_entity_list(data)
# For simplicity, only service calls affecting a single entity are currently handled.
From f4d872a54089a05f84292e0f58cb8e9d95562181 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 30 Jul 2023 13:01:08 -0700
Subject: [PATCH 079/549] Make sure that SimpleSwitches are added before
AdaptiveSwitch (#686)
---
custom_components/adaptive_lighting/switch.py | 24 +++++++++++++++----
tests/test_switch.py | 3 +--
2 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 58638c3d..2cf2c5db 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -293,7 +293,7 @@ def _switches_with_lights(
entry = data.get(config.entry_id)
if entry is None: # entry might be disabled and therefore missing
continue
- switch = data[config.entry_id]["instance"]
+ switch = data[config.entry_id][SWITCH_DOMAIN]
switch._expand_light_groups()
# Check if any of the lights are in the switch's lights
if set(switch.lights) & set(all_check_lights):
@@ -367,7 +367,7 @@ def _switches_from_service_call(
ent_entry = ent_reg.async_get(entity_id)
assert ent_entry is not None
config_id = ent_entry.config_entry_id
- switches.append(hass.data[DOMAIN][config_id]["instance"])
+ switches.append(hass.data[DOMAIN][config_id][SWITCH_DOMAIN])
return switches
if lights:
@@ -498,9 +498,6 @@ async def async_setup_entry( # noqa: PLR0915
adapt_brightness_switch,
)
- # save our switch instance, allows us to make switch's entity_id optional in service calls.
- hass.data[DOMAIN][config_entry.entry_id]["instance"] = switch
-
data[config_entry.entry_id][SLEEP_MODE_SWITCH] = sleep_mode_switch
data[config_entry.entry_id][ADAPT_COLOR_SWITCH] = adapt_color_switch
data[config_entry.entry_id][ADAPT_BRIGHTNESS_SWITCH] = adapt_brightness_switch
@@ -1016,6 +1013,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
_LOGGER.debug("%s: Cancelled '_setup_listeners'", self._name)
return
+ while not all(
+ sw._state is not None
+ for sw in [
+ self.sleep_mode_switch,
+ self.adapt_brightness_switch,
+ self.adapt_color_switch,
+ ]
+ ):
+ # Waits until `async_added_to_hass` is done, which in SimpleSwitch
+ # is when `_state` is set to `True` or `False`.
+ # Fixes first issue in https://github.com/basnijholt/adaptive-lighting/issues/682
+ _LOGGER.debug(
+ "%s: Waiting for simple switches to be initialized",
+ self._name,
+ )
+ await asyncio.sleep(0.1)
+
assert not self.remove_listeners
self._update_time_interval_listener()
diff --git a/tests/test_switch.py b/tests/test_switch.py
index be0c7d37..2a49f41c 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -331,9 +331,8 @@ async def test_adaptive_lighting_switches(hass):
assert ADAPT_COLOR_SWITCH in data
assert ADAPT_BRIGHTNESS_SWITCH in data
assert UNDO_UPDATE_LISTENER in data
- assert "instance" in data
- assert len(data.keys()) == 6
+ assert len(data.keys()) == 5
@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS)
From e98f0dfa550f6f2d7e4b00a61e8ec7767a860a53 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 30 Jul 2023 13:11:30 -0700
Subject: [PATCH 080/549] Bump to 1.18.1
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 34045b68..87996208 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.18.0"
+ "version": "1.18.1"
}
From 1842ac9bd5db62e7af8fd2ead12121abd870edb2 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 30 Jul 2023 13:28:58 -0700
Subject: [PATCH 081/549] Do not assert but issue a warning (#687)
* Do not assert but issue a warning
* bump to 1.18.2
---
custom_components/adaptive_lighting/manifest.json | 2 +-
custom_components/adaptive_lighting/switch.py | 11 ++++++++++-
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 87996208..8d6a80b9 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.18.1"
+ "version": "1.18.2"
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 2cf2c5db..a96351f2 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -2476,7 +2476,16 @@ class AdaptiveLightingManager:
off_to_on_event: Event,
) -> bool:
# Adaptive Lighting should never turn on lights itself
- assert not is_our_context(off_to_on_event.context)
+ if is_our_context(off_to_on_event.context):
+ _LOGGER.warning(
+ "Detected an 'off' → 'on' event for '%s' with context.id='%s' and"
+ " event='%s', triggered by the adaptive_lighting integration itself,"
+ " which *should* not happen. If you see this please submit an issue with"
+ " your full logs at https://github.com/basnijholt/adaptive-lighting",
+ entity_id,
+ off_to_on_event.context.id,
+ off_to_on_event,
+ )
turn_on_event: Event | None = self.turn_on_event.get(entity_id)
id_off_to_on = off_to_on_event.context.id
return (
From d588a5984ad2eb3ecf3fe5e15c2d8ec9f560e7f2 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 30 Jul 2023 21:57:30 -0700
Subject: [PATCH 082/549] Refactor _update_attrs_and_maybe_adapt_lights (#688)
* Do not call self.manager.significant_change when not needed
* refact
* fix
* fix
* Add more logging to tests
* remove
---
custom_components/adaptive_lighting/switch.py | 79 ++++++++++---------
tests/test_switch.py | 9 ++-
2 files changed, 49 insertions(+), 39 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index a96351f2..4229f0ac 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1376,7 +1376,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
data,
)
- async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912
+ async def _update_attrs_and_maybe_adapt_lights(
self,
*,
context: Context,
@@ -1403,17 +1403,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
self.async_write_ha_state()
- if lights is None:
- lights = self.lights
-
if not force and self._only_once:
return
+ if lights is None:
+ lights = self.lights
+
+ on_lights = [light for light in lights if is_on(self.hass, light)]
+
if force:
- filtered_lights = lights
+ filtered_lights = on_lights
else:
filtered_lights = []
- for light in lights:
+ for light in on_lights:
# Don't adapt lights that haven't finished prior transitions.
timer = self.manager.transition_timers.get(light)
if timer is not None and timer.is_running():
@@ -1428,25 +1430,35 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
_LOGGER.debug("%s: filtered_lights: '%s'", self._name, filtered_lights)
if not filtered_lights:
return
+
adapt_brightness = self.adapt_brightness_switch.is_on
adapt_color = self.adapt_color_switch.is_on
assert isinstance(adapt_brightness, bool)
assert isinstance(adapt_color, bool)
for light in filtered_lights:
- if not is_on(self.hass, light):
+ manually_controlled = (
+ self._take_over_control
+ and self.manager.is_manually_controlled(
+ self,
+ light,
+ force,
+ adapt_brightness,
+ adapt_color,
+ )
+ )
+ if manually_controlled:
+ _LOGGER.debug(
+ "%s: '%s' is being manually controlled, stop adapting, context.id=%s.",
+ self._name,
+ light,
+ context.id,
+ )
continue
- manually_controlled = self.manager.is_manually_controlled(
- self,
- light,
- force,
- adapt_brightness,
- adapt_color,
- )
-
significant_change = (
- self._detect_non_ha_changes
+ self._take_over_control
+ and self._detect_non_ha_changes
and not force
and await self.manager.significant_change(
self,
@@ -1456,28 +1468,19 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context,
)
)
+ if significant_change:
+ _fire_manual_control_event(self, light, context)
+ continue
- if self._take_over_control and (manually_controlled or significant_change):
- if manually_controlled:
- _LOGGER.debug(
- "%s: '%s' is being manually controlled, stop adapting, context.id=%s.",
- self._name,
- light,
- context.id,
- )
- else:
- # Need to fire manual control event because of significant_change
- _fire_manual_control_event(self, light, context)
- else:
- _LOGGER.debug(
- "%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:"
- " '%s' with transition %s and context.id=%s",
- self._name,
- light,
- transition,
- context.id,
- )
- await self._adapt_light(light, context, transition)
+ _LOGGER.debug(
+ "%s: Calling _adapt_light from _update_attrs_and_maybe_adapt_lights:"
+ " '%s' with transition %s and context.id=%s",
+ self._name,
+ light,
+ transition,
+ context.id,
+ )
+ await self._adapt_light(light, context, transition)
async def _sleep_mode_switch_state_event_action(self, event: Event) -> None:
if not _is_state_event(event, (STATE_ON, STATE_OFF)):
@@ -2417,7 +2420,7 @@ class AdaptiveLightingManager:
light: str,
adapt_brightness: bool,
adapt_color: bool,
- context: Context,
+ context: Context, # just for logging
) -> bool:
"""Has the light made a significant change since last update.
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 2a49f41c..43da74a2 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -546,6 +546,9 @@ async def test_manager_not_tracking_untracked_lights(hass):
async def test_manual_control(hass):
"""Test the 'manual control' tracking."""
switch, (light, *_) = await setup_lights_and_switch(hass)
+ assert switch._take_over_control
+ assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
+
context = switch.create_context("test") # needs to be passed to update method
manual_control = switch.manager.manual_control
@@ -560,9 +563,9 @@ async def test_manual_control(hass):
{ATTR_ENTITY_ID: ENTITY_LIGHT_1, **kwargs},
blocking=True,
)
+ _LOGGER.debug("Turn light %s, to %s", "on" if state else "off", kwargs)
await hass.async_block_till_done()
await update()
- _LOGGER.debug("Turn light %s, to %s", state, kwargs)
async def turn_switch(state, entity_id):
await hass.services.async_call(
@@ -576,6 +579,7 @@ async def test_manual_control(hass):
async def change_manual_control(set_to, extra_service_data=None):
if extra_service_data is None:
extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT_1]}
+ _LOGGER.debug(f"{switch.manager.manual_control=}")
await hass.services.async_call(
DOMAIN,
SERVICE_SET_MANUAL_CONTROL,
@@ -586,8 +590,11 @@ async def test_manual_control(hass):
},
blocking=True,
)
+ _LOGGER.debug(f"{switch.manager.manual_control=}")
+ _LOGGER.debug("Called set_manual_control with %s", set_to)
await hass.async_block_till_done()
await update()
+ _LOGGER.debug("End of change_manual_control")
def increased_brightness():
return (light._attr_brightness + 100) % 255
From aa9f69a7bbcfb471094778ee3e4a9ab369c233bf Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 31 Jul 2023 17:52:53 -0700
Subject: [PATCH 083/549] Set last_service_data in the right place (#652)
* Set last_service_data in the right place
* Do not get but access key
* rename
---
custom_components/adaptive_lighting/switch.py | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 4229f0ac..7bcd78bb 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1262,8 +1262,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
context = context or self.create_context("adapt_lights")
- self.manager.last_service_data[light] = service_data
-
return prepare_adaptation_data(
self.hass,
light,
@@ -1337,6 +1335,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
service_data,
data.context.id,
)
+ light = service_data[ATTR_ENTITY_ID]
+ self.manager.last_service_data[light] = service_data
await self.hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
@@ -2134,10 +2134,7 @@ class AdaptiveLightingManager:
def start_transition_timer(self, light: str) -> None:
"""Mark a light as manually controlled."""
- last_service_data = self.last_service_data.get(light)
- if not last_service_data:
- _LOGGER.debug("This should not ever happen. Please report to the devs.")
- return
+ last_service_data = self.last_service_data[light]
last_transition = last_service_data.get(ATTR_TRANSITION)
if not last_transition:
_LOGGER.debug(
From da2e27d7381b12e9362daa2a6eb76e0cb6d97277 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 31 Jul 2023 18:15:36 -0700
Subject: [PATCH 084/549] [pre-commit.ci] pre-commit autoupdate (#691)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.280 → v0.0.281](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.280...v0.0.281)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index dc82bbe7..6e27d970 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,7 +8,7 @@ repos:
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.0.280
+ rev: v0.0.281
hooks:
- id: ruff
args: ["--fix"]
From e5133936536cb02d6e8122c1d7dc5fde5810214c Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Mon, 31 Jul 2023 18:40:32 -0700
Subject: [PATCH 085/549] Keep on and off state tracking in Manager and listen
to toggle (#689)
* Keep on and off state tracking in Manager
* Move manual_control code
* add todo
* check is_on
* Track toggle
* handle toggle
* fix debug
* test
* add comment
* better log
* Fix comment
* log
* test
* get
* check
* cancel early
* rephrase
* add assert
* Remove check
---
custom_components/adaptive_lighting/switch.py | 312 +++++++++---------
tests/test_switch.py | 14 +-
2 files changed, 166 insertions(+), 160 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 7bcd78bb..772aa1e0 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -859,12 +859,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._icon = ICON_MAIN
self._state: bool | None = None
- # Tracks 'on' → 'off' state changes
- self._on_to_off_event: dict[str, Event] = {}
- # Tracks 'off' → 'on' state changes
- self._off_to_on_event: dict[str, Event] = {}
- # Locks that prevent light adjusting when waiting for a light to 'turn_off'
- self._locks: dict[str, asyncio.Lock] = {}
# To count the number of `Context` instances
self._context_cnt: int = 0
@@ -1041,15 +1035,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
self.remove_listeners.append(remove_sleep)
-
- if self.lights:
- self._expand_light_groups()
- remove_state = async_track_state_change_event(
- self.hass,
- entity_ids=self.lights,
- action=self._light_state_event_action,
- )
- self.remove_listeners.append(remove_state)
+ self._expand_light_groups()
def _update_time_interval_listener(self) -> None:
"""Create or recreate the adaptation interval listener.
@@ -1282,19 +1268,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
adapt_color: bool | None = None,
prefer_rgb_color: bool | None = None,
) -> None:
- if (lock := self._locks.get(light)) is not None and lock.locked():
- _LOGGER.debug("%s: '%s' is locked", self._name, light)
- return
+ # This should never happen if it's been proactively adapted.
+ # The context.parent_id is the context.id of the service call that was intercepted
+ # and context.id here is from the resulting "light_event" event.
+ assert not self.manager.is_proactively_adapting(context.parent_id)
- if context.parent_id is not None and self.manager.is_proactively_adapting(
- context.parent_id,
- ):
- # Skip if adaptation was already executed by the service call interceptor
- _LOGGER.debug(
- "%s: Skipping reactive adaptation of %s",
- self._name,
- context.parent_id,
- )
+ if (lock := self.manager.turn_off_locks.get(light)) and lock.locked():
+ _LOGGER.debug("%s: '%s' is locked", self._name, light)
return
data = await self.prepare_adaptation_data(
@@ -1482,6 +1462,39 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
)
await self._adapt_light(light, context, transition)
+ async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None:
+ assert not self.manager.is_proactively_adapting(event.context.id)
+ if (
+ not self._detect_non_ha_changes
+ and not self.manager._off_to_on_state_event_is_from_turn_on(
+ entity_id,
+ event,
+ )
+ ):
+ # There is an edge case where 2 switches control the same light, e.g.,
+ # one for brightness and one for color. Now we will mark both switches
+ # as manually controlled, which is not 100% correct.
+ _LOGGER.debug(
+ "%s: Ignoring 'off' → 'on' event for '%s' with context.id='%s'"
+ " because 'light.turn_on' was not called by HA and"
+ " 'detect_non_ha_changes' is False",
+ self._name,
+ entity_id,
+ event.context.id,
+ )
+ self.manager.mark_as_manual_control(entity_id)
+ return
+
+ if self._adapt_delay > 0:
+ await asyncio.sleep(self._adapt_delay)
+
+ await self._update_attrs_and_maybe_adapt_lights(
+ context=self.create_context("light_event", parent=event.context),
+ lights=[entity_id],
+ transition=self.initial_transition,
+ force=True,
+ )
+
async def _sleep_mode_switch_state_event_action(self, event: Event) -> None:
if not _is_state_event(event, (STATE_ON, STATE_OFF)):
_LOGGER.debug("%s: Ignoring sleep event %s", self._name, event)
@@ -1499,102 +1512,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
force=True,
)
- async def _light_state_event_action(self, event: Event) -> None:
- old_state = event.data.get("old_state")
- new_state = event.data.get("new_state")
- entity_id: str = event.data["entity_id"]
-
- if old_state is None or new_state is None:
- return
-
- if old_state.state == STATE_ON and new_state.state == STATE_OFF:
- # Tracks 'on' → 'off' state changes
- self._on_to_off_event[entity_id] = event
- self.manager.reset(entity_id)
- _LOGGER.debug(
- "%s: Detected an 'on' → 'off' event for '%s' with context.id='%s'",
- self._name,
- entity_id,
- event.context.id,
- )
-
- if old_state.state == STATE_OFF and new_state.state == STATE_ON:
- # Tracks 'off' → 'on' state changes
- self._off_to_on_event[entity_id] = event
- _LOGGER.debug(
- "%s: Detected an 'off' → 'on' event for '%s' with context.id='%s'",
- self._name,
- entity_id,
- event.context.id,
- )
-
- if (
- not self._detect_non_ha_changes
- and not self.manager.is_proactively_adapting(event.context.id)
- and not self.manager._off_to_on_state_event_is_from_turn_on(
- entity_id,
- event,
- )
- ):
- # If we don't detect non-HA changes, we're only adjusting lights that
- # were turned on by HA. If the light was turned on by something else,
- # we don't adjust it (e.g., when HA suddenly reports it as on).
- # Sometimes the light incorrectly reports itself as on when it's
- # actually off. This code path will ensure that the light is
- # not controlled by Adaptive Lighting.
- _LOGGER.debug(
- "%s: Ignoring 'off' → 'on' event for '%s' with context.id='%s'"
- " because 'light.turn_on' was not called by HA and"
- " 'detect_non_ha_changes' is False",
- self._name,
- entity_id,
- event.context.id,
- )
- self.manager.mark_as_manual_control(entity_id)
- return
-
- if event.context.parent_id and not self.manager.is_proactively_adapting(
- event.context.id,
- ):
- self.manager.reset(entity_id, reset_manual_control=False)
-
- lock = self._locks.setdefault(entity_id, asyncio.Lock())
- async with lock:
- if await self.manager.just_turned_off(
- entity_id,
- off_to_on_event=event,
- on_to_off_event=self._on_to_off_event.get(entity_id),
- ):
- # Stop if a rapid 'off' → 'on' → 'off' happens.
- _LOGGER.debug(
- "%s: Cancelling adjusting lights for %s",
- self._name,
- entity_id,
- )
- return
-
- if self._adapt_delay > 0:
- _LOGGER.debug(
- "%s: sleep started for '%s' with context.id='%s'",
- self._name,
- entity_id,
- event.context.id,
- )
- await asyncio.sleep(self._adapt_delay)
- _LOGGER.debug(
- "%s: sleep ended for '%s' with context.id='%s'",
- self._name,
- entity_id,
- event.context.id,
- )
-
- await self._update_attrs_and_maybe_adapt_lights(
- context=self.create_context("light_event", parent=event.context),
- lights=[entity_id],
- transition=self.initial_transition,
- force=True,
- )
-
class SimpleSwitch(SwitchEntity, RestoreEntity):
"""Representation of a Adaptive Lighting switch."""
@@ -1908,12 +1825,20 @@ class AdaptiveLightingManager:
self.turn_off_event: dict[str, Event] = {}
# Tracks 'light.turn_on' service calls
self.turn_on_event: dict[str, Event] = {}
+ # Tracks 'light.toggle' service calls
+ self.toggle_event: dict[str, Event] = {}
+ # Tracks 'on' → 'off' state changes
+ self.on_to_off_event: dict[str, Event] = {}
+ # Tracks 'off' → 'on' state changes
+ self.off_to_on_event: dict[str, Event] = {}
# Keep 'asyncio.sleep' tasks that can be cancelled by 'light.turn_on' events
self.sleep_tasks: dict[str, asyncio.Task] = {}
+ # Locks that prevent light adjusting when waiting for a light to 'turn_off'
+ self.turn_off_locks: dict[str, asyncio.Lock] = {}
# Tracks which lights are manually controlled
self.manual_control: dict[str, bool] = {}
# Track 'state_changed' events of self.lights resulting from this integration
- self.last_state_change: dict[str, list[State]] = {}
+ self.our_last_state_on_change: dict[str, list[State]] = {}
# Track last 'service_data' to 'light.turn_on' resulting from this integration
self.last_service_data: dict[str, dict[str, Any]] = {}
# Track ongoing split adaptations to be able to cancel them
@@ -2069,7 +1994,11 @@ class AdaptiveLightingManager:
call.context.id,
)
+ # Reset because turning on the light, this also happens in
+ # `turn_on_off_event_listener`, however, this function is called
+ # before that one.
self.reset(entity_id, reset_manual_control=False)
+
self.clear_proactively_adapting(entity_id)
transition = data[CONF_PARAMS].get(
@@ -2243,7 +2172,7 @@ class AdaptiveLightingManager:
timer = self.auto_reset_manual_control_timers.pop(light, None)
if timer is not None:
timer.cancel()
- self.last_state_change.pop(light, None)
+ self.our_last_state_on_change.pop(light, None)
self.last_service_data.pop(light, None)
self.cancel_ongoing_adaptation_calls(light)
@@ -2286,6 +2215,24 @@ class AdaptiveLightingManager:
if not any(eid in self.lights for eid in entity_ids):
return
+ def off(eid: str, event: Event):
+ self.turn_off_event[eid] = event
+ self.reset(eid)
+
+ def on(eid: str, event: Event):
+ task = self.sleep_tasks.get(eid)
+ if task is not None:
+ task.cancel()
+ self.turn_on_event[eid] = event
+ timer = self.auto_reset_manual_control_timers.get(eid)
+ if (
+ timer is not None
+ and timer.is_running()
+ and event.time_fired > timer.start_time # type: ignore[operator]
+ ):
+ # Restart the auto reset timer
+ timer.start()
+
if service == SERVICE_TURN_OFF:
transition = service_data.get(ATTR_TRANSITION)
_LOGGER.debug(
@@ -2295,8 +2242,7 @@ class AdaptiveLightingManager:
event.context.id,
)
for eid in entity_ids:
- self.turn_off_event[eid] = event
- self.reset(eid)
+ off(eid, event)
elif service == SERVICE_TURN_ON:
_LOGGER.debug(
@@ -2305,18 +2251,21 @@ class AdaptiveLightingManager:
event.context.id,
)
for eid in entity_ids:
- task = self.sleep_tasks.get(eid)
- if task is not None:
- task.cancel()
- self.turn_on_event[eid] = event
- timer = self.auto_reset_manual_control_timers.get(eid)
- if (
- timer is not None
- and timer.is_running()
- and event.time_fired > timer.start_time # type: ignore[operator]
- ):
- # Restart the auto reset timer
- timer.start()
+ on(eid, event)
+
+ elif service == SERVICE_TOGGLE:
+ _LOGGER.debug(
+ "Detected an 'light.toggle('%s')' event with context.id='%s'",
+ entity_ids,
+ event.context.id,
+ )
+ for eid in entity_ids:
+ state = self.hass.states.get(eid).state
+ self.toggle_event[eid] = event
+ if state == STATE_ON: # is turning off
+ off(eid, event)
+ elif state == STATE_OFF: # is turning on
+ on(eid, event)
async def state_changed_event_listener(self, event: Event) -> None:
"""Track 'state_changed' events."""
@@ -2324,16 +2273,21 @@ class AdaptiveLightingManager:
if entity_id not in self.lights:
return
+ old_state = event.data.get("old_state")
new_state = event.data.get("new_state")
- if new_state is not None and new_state.state == STATE_ON:
+
+ new_on = new_state is not None and new_state.state == STATE_ON
+ new_off = new_state is not None and new_state.state == STATE_OFF
+ old_on = old_state is not None and old_state.state == STATE_ON
+ old_off = old_state is not None and old_state.state == STATE_OFF
+
+ if new_on:
_LOGGER.debug(
"Detected a '%s' 'state_changed' event: '%s' with context.id='%s'",
entity_id,
new_state.attributes,
new_state.context.id,
)
-
- if new_state is not None and new_state.state == STATE_ON:
# It is possible to have multiple state change events with the same context.
# This can happen because a `turn_on.light(brightness_pct=100, transition=30)`
# event leads to an instant state change of
@@ -2345,30 +2299,78 @@ class AdaptiveLightingManager:
# called with a color_temp outside of its range (and HA reports the
# incorrect 'min_kelvin' and 'max_kelvin', which happens e.g., for
# Philips Hue White GU10 Bluetooth lights).
- old_state: list[State] | None = self.last_state_change.get(entity_id)
+ last_state: list[State] | None = self.our_last_state_on_change.get(
+ entity_id,
+ )
if is_our_context(new_state.context):
if (
- old_state is not None
- and old_state[0].context.id == new_state.context.id
+ last_state is not None
+ and last_state[0].context.id == new_state.context.id
):
_LOGGER.debug(
"AdaptiveLightingManager: State change event of '%s' is already"
- " in 'self.last_state_change' (%s)"
+ " in 'self.our_last_state_on_change' (%s)"
" adding this state also",
entity_id,
new_state.context.id,
)
- self.last_state_change[entity_id].append(new_state)
+ self.our_last_state_on_change[entity_id].append(new_state)
else:
_LOGGER.debug(
"AdaptiveLightingManager: New adapt '%s' found for %s",
new_state,
entity_id,
)
- self.last_state_change[entity_id] = [new_state]
+ self.our_last_state_on_change[entity_id] = [new_state]
self.start_transition_timer(entity_id)
- elif old_state is not None:
- self.last_state_change[entity_id].append(new_state)
+ elif last_state is not None:
+ self.our_last_state_on_change[entity_id].append(new_state)
+
+ if old_on and new_off:
+ # Tracks 'on' → 'off' state changes
+ self.on_to_off_event[entity_id] = event
+ self.reset(entity_id)
+ _LOGGER.debug(
+ "Detected an 'on' → 'off' event for '%s' with context.id='%s'",
+ entity_id,
+ event.context.id,
+ )
+ elif old_off and new_on:
+ # Tracks 'off' → 'on' state changes
+ self.off_to_on_event[entity_id] = event
+ _LOGGER.debug(
+ "Detected an 'off' → 'on' event for '%s' with context.id='%s'",
+ entity_id,
+ event.context.id,
+ )
+
+ if self.is_proactively_adapting(event.context.id):
+ _LOGGER.debug(
+ "Skipping responding to 'off' → 'on' event for '%s' with context.id='%s' because"
+ " we are already proactively adapting",
+ entity_id,
+ event.context.id,
+ )
+ return
+
+ self.reset(entity_id, reset_manual_control=False)
+ lock = self.turn_off_locks.setdefault(entity_id, asyncio.Lock())
+ async with lock:
+ if await self.just_turned_off(entity_id):
+ # Stop if a rapid 'off' → 'on' → 'off' happens.
+ _LOGGER.debug(
+ "Cancelling adjusting lights for %s",
+ entity_id,
+ )
+ return
+
+ switches = _switches_with_lights(self.hass, [entity_id])
+ for switch in switches:
+ if switch.is_on:
+ await switch._respond_to_off_to_on_event(
+ entity_id,
+ event,
+ )
def is_manually_controlled(
self,
@@ -2497,8 +2499,6 @@ class AdaptiveLightingManager:
async def just_turned_off( # noqa: PLR0911
self,
entity_id: str,
- off_to_on_event: Event,
- on_to_off_event: Event | None,
) -> bool:
"""Cancel the adjusting of a light if it has just been turned off.
@@ -2512,6 +2512,9 @@ class AdaptiveLightingManager:
if the brightness is still decreasing. Only if it is the case we
adjust the lights.
"""
+ off_to_on_event = self.off_to_on_event[entity_id]
+ on_to_off_event = self.on_to_off_event.get(entity_id)
+
if on_to_off_event is None:
_LOGGER.debug(
"just_turned_off: No 'on' → 'off' state change has been registered before for '%s'."
@@ -2529,8 +2532,11 @@ class AdaptiveLightingManager:
transition = None
if self._off_to_on_state_event_is_from_turn_on(entity_id, off_to_on_event):
+ is_toggle = off_to_on_event == self.toggle_event.get(entity_id)
+ from_service = "light.toggle" if is_toggle else "light.turn_on"
_LOGGER.debug(
- "just_turned_off: State change 'off' → 'on' triggered by 'light.turn_on'",
+ "just_turned_off: State change 'off' → 'on' triggered by '%s'",
+ from_service,
)
return False
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 43da74a2..c7220ee4 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -990,8 +990,8 @@ async def test_state_change_handlers(hass):
blocking=True,
)
await hass.async_block_till_done()
- assert switch.manager.last_state_change.get(ENTITY_LIGHT_1)
- assert len(switch.manager.last_state_change[ENTITY_LIGHT_1]) == 1
+ assert switch.manager.our_last_state_on_change.get(ENTITY_LIGHT_1)
+ assert len(switch.manager.our_last_state_on_change[ENTITY_LIGHT_1]) == 1
assert not switch.manager.transition_timers.get(ENTITY_LIGHT_1)
last_service_data = deepcopy(switch.manager.last_service_data)
assert last_service_data.get(ENTITY_LIGHT_1)
@@ -1060,8 +1060,8 @@ async def test_state_change_handlers(hass):
# asyncio.sleep(3)
# 4. Assert the transition timer started and everything was filled.
listener = switch.manager
- assert listener.last_state_change.get(ENTITY_LIGHT_1)
- assert len(listener.last_state_change[ENTITY_LIGHT_1]) == total_events
+ assert listener.our_last_state_on_change.get(ENTITY_LIGHT_1)
+ assert len(listener.our_last_state_on_change[ENTITY_LIGHT_1]) == total_events
assert listener.transition_timers.get(ENTITY_LIGHT_1)
# 5. Execute some checks during a transition
@@ -1086,8 +1086,8 @@ async def test_state_change_handlers(hass):
# 6. Assert everything after the transition finishes.
await asyncio.sleep(transition_used)
- assert listener.last_state_change.get(ENTITY_LIGHT_1)
- assert len(listener.last_state_change[ENTITY_LIGHT_1]) == total_events
+ assert listener.our_last_state_on_change.get(ENTITY_LIGHT_1)
+ assert len(listener.our_last_state_on_change[ENTITY_LIGHT_1]) == 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_1)
@@ -1115,7 +1115,7 @@ async def test_state_change_handlers(hass):
# On next update ENTITY_LIGHT_1 should be marked as manually controlled
await update(force=False)
assert switch.manager.last_service_data.get(ENTITY_LIGHT_1) is not None
- assert switch.manager.last_state_change.get(ENTITY_LIGHT_1) is not None
+ assert switch.manager.our_last_state_on_change.get(ENTITY_LIGHT_1) is not None
assert switch.manager.manual_control[ENTITY_LIGHT_1]
From 8d13f1387346fd6dd79caef4a44a140e718996b0 Mon Sep 17 00:00:00 2001
From: badboybeyer
Date: Wed, 2 Aug 2023 08:09:34 -0700
Subject: [PATCH 086/549] bug: fix bug in comment in readme (#693)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 8721fc42..9a70b89a 100644
--- a/README.md
+++ b/README.md
@@ -148,7 +148,7 @@ adaptive_lighting:
sunrise_time: "08:00:00" # override the sunrise time
sunrise_offset:
sunset_time:
- sunset_offset: 1800 # in seconds or '00:15:00'
+ sunset_offset: 1800 # in seconds or '00:30:00'
take_over_control: true
detect_non_ha_changes: false
only_once: false
From 807f7109d49898b2bbebcec3057eb1017bf21099 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 2 Aug 2023 17:59:44 -0700
Subject: [PATCH 087/549] Require take_over_control for to ignore non-turn_on
state off -> on event (#695)
---
custom_components/adaptive_lighting/switch.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 772aa1e0..86f430bc 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1465,7 +1465,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None:
assert not self.manager.is_proactively_adapting(event.context.id)
if (
- not self._detect_non_ha_changes
+ self._take_over_control
+ and not self._detect_non_ha_changes
and not self.manager._off_to_on_state_event_is_from_turn_on(
entity_id,
event,
From 53ed220fa0802c87f3cdd3aafa96bfd2d51ced13 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 2 Aug 2023 22:59:01 -0700
Subject: [PATCH 088/549] Bail adapting if on event equals off event context
(#696)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Bail adapting if on event equals off event context
Should prevent this (I saw in my logs)
```
2023-08-02 21:49:56.516 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'light.turn_off('['light.philips_go', 'light.bed_led', 'light.bamboo']', transition=10.0)' event with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:49:56.637 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'on' → 'off' event for 'light.bamboo' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:49:56.672 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'on' → 'off' event for 'light.bed_led' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:49:56.747 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'on' → 'off' event for 'light.philips_go' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.501 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected a 'light.philips_go' 'state_changed' event: '{'min_color_temp_kelvin': 2000, 'max_color_temp_kelvin': 6666, 'min_mireds': 150, 'max_mireds': 500, 'effect_list': ['blink', 'breathe', 'okay', 'channel_change', 'candle', 'fireplace', 'colorloop', 'finish_effect', 'stop_effect', 'stop_hue_effect'], 'supported_color_modes': ['color_temp', 'xy'], 'color_mode': , 'brightness': 10, 'hs_color': (0.0, 100.0), 'rgb_color': (255, 0, 0), 'xy_color': (0.701, 0.299), 'friendly_name': 'Philips Go', 'supported_features': }' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.502 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'off' → 'on' event for 'light.philips_go' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.502 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] is_proactively_adapting_context='False', context_id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.502 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] just_turned_off: Waiting with adjusting 'light.philips_go' for 6.240101
2023-08-02 21:50:00.528 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected a 'light.bed_led' 'state_changed' event: '{'min_color_temp_kelvin': 2000, 'max_color_temp_kelvin': 6535, 'min_mireds': 153, 'max_mireds': 500, 'effect_list': ['blink', 'breathe', 'okay', 'channel_change', 'candle', 'fireplace', 'colorloop', 'finish_effect', 'stop_effect', 'stop_hue_effect'], 'supported_color_modes': ['color_temp', 'xy'], 'color_mode': , 'brightness': 10, 'hs_color': (10.824, 100.0), 'rgb_color': (255, 46, 0), 'xy_color': (0.689, 0.309), 'friendly_name': 'Bed LED', 'supported_features': }' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.529 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'off' → 'on' event for 'light.bed_led' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.529 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] is_proactively_adapting_context='False', context_id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.529 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] just_turned_off: Waiting with adjusting 'light.bed_led' for 6.129017
2023-08-02 21:50:00.561 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected a 'light.bamboo' 'state_changed' event: '{'min_color_temp_kelvin': 2000, 'max_color_temp_kelvin': 6535, 'min_mireds': 153, 'max_mireds': 500, 'effect_list': ['blink', 'breathe', 'okay', 'channel_change', 'candle', 'fireplace', 'colorloop', 'finish_effect', 'stop_effect', 'stop_hue_effect'], 'supported_color_modes': ['color_temp', 'xy'], 'color_mode': , 'brightness': 10, 'hs_color': (299.434, 83.137), 'rgb_color': (253, 43, 255), 'xy_color': (0.382, 0.159), 'friendly_name': 'Bamboo', 'supported_features': }' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.562 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] Detected an 'off' → 'on' event for 'light.bamboo' with context.id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.562 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] is_proactively_adapting_context='False', context_id='01H6WVP8RJF4JZ78SFD98YA0RG'
2023-08-02 21:50:00.562 DEBUG (MainThread) [custom_components.adaptive_lighting.switch] just_turned_off: Waiting with adjusting 'light.bamboo' for 6.072956
```
* log instead
---
custom_components/adaptive_lighting/switch.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 86f430bc..b6f8a24e 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -2497,7 +2497,7 @@ class AdaptiveLightingManager:
and id_off_to_on == turn_on_event.context.id
)
- async def just_turned_off( # noqa: PLR0911
+ async def just_turned_off( # noqa: PLR0911, PLR0912
self,
entity_id: str,
) -> bool:
@@ -2524,6 +2524,14 @@ class AdaptiveLightingManager:
)
return False
+ if off_to_on_event.context.id == on_to_off_event.context.id:
+ _LOGGER.debug(
+ "just_turned_off: 'on' → 'off' state change has the same context.id as the"
+ " 'off' → 'on' state change for '%s'. This is probably a false positive.",
+ entity_id,
+ )
+ return True
+
id_on_to_off = on_to_off_event.context.id
turn_off_event = self.turn_off_event.get(entity_id)
From e0e04da0f6ea230fa4738d24fc456696c2806a7b Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 2 Aug 2023 23:11:45 -0700
Subject: [PATCH 089/549] Before scheduling turn_on do a last-minute check if
lights are off (#671)
* Before scheduling turn_on do a last-minute check if lights are off
* Pass force
* add comment
* fix for proactive
* commetn
* No default for force
* rm newline
* no force
* Return bool
---
.../adaptive_lighting/adaptation_utils.py | 3 +++
custom_components/adaptive_lighting/switch.py | 23 ++++++++++++++++++-
tests/test_adaptation_utils.py | 1 +
tests/test_switch.py | 1 +
4 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py
index e0ab6c47..97914327 100644
--- a/custom_components/adaptive_lighting/adaptation_utils.py
+++ b/custom_components/adaptive_lighting/adaptation_utils.py
@@ -141,6 +141,7 @@ class AdaptationData:
context: Context
sleep_time: float
service_call_datas: AsyncGenerator[ServiceData, None]
+ force: bool
max_length: int
which: Literal["brightness", "color", "both"]
initial_sleep: bool = False
@@ -179,6 +180,7 @@ def prepare_adaptation_data(
service_data: ServiceData,
split: bool,
filter_by_state: bool,
+ force: bool,
) -> AdaptationData:
"""Prepares a data object carrying all data required to execute an adaptation."""
_LOGGER.debug(
@@ -209,6 +211,7 @@ def prepare_adaptation_data(
context=context,
sleep_time=sleep_time,
service_call_datas=service_data_iterator,
+ force=force,
max_length=service_datas_length,
which=lighting_type,
)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index b6f8a24e..641ffab6 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -537,6 +537,7 @@ async def async_setup_entry( # noqa: PLR0915
adapt_brightness=data[ATTR_ADAPT_BRIGHTNESS],
adapt_color=data[ATTR_ADAPT_COLOR],
prefer_rgb_color=data[CONF_PREFER_RGB_COLOR],
+ force=True,
)
@callback
@@ -1171,6 +1172,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
adapt_brightness: bool | None = None,
adapt_color: bool | None = None,
prefer_rgb_color: bool | None = None,
+ force: bool = False,
context: Context | None = None,
) -> AdaptationData | None:
"""Prepare `AdaptationData` for adapting a light."""
@@ -1257,6 +1259,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
service_data,
split=self._separate_turn_on_commands,
filter_by_state=self._skip_redundant_commands,
+ force=force,
)
async def _adapt_light(
@@ -1267,6 +1270,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
adapt_brightness: bool | None = None,
adapt_color: bool | None = None,
prefer_rgb_color: bool | None = None,
+ force: bool = False,
) -> None:
# This should never happen if it's been proactively adapted.
# The context.parent_id is the context.id of the service call that was intercepted
@@ -1283,6 +1287,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
adapt_brightness,
adapt_color,
prefer_rgb_color,
+ force,
context,
)
if data is None:
@@ -1308,6 +1313,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
# All service datas processed
break
+ if (
+ not data.force
+ and not is_on(self.hass, data.entity_id)
+ # if proactively adapting, we are sure that it came from a `light.turn_on`
+ and not self.manager.is_proactively_adapting(data.context.id)
+ ):
+ # Do a last-minute check if the entity is still on.
+ _LOGGER.debug(
+ "%s: Skipping adaptation of %s because it is now off",
+ self._name,
+ data.entity_id,
+ )
+ return
+
_LOGGER.debug(
"%s: Scheduling 'light.turn_on' with the following 'service_data': %s"
" with context.id='%s'",
@@ -1440,6 +1459,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._take_over_control
and self._detect_non_ha_changes
and not force
+ # Note: This call updates the state of the light
+ # so it might suddenly be off.
and await self.manager.significant_change(
self,
light,
@@ -1460,7 +1481,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
transition,
context.id,
)
- await self._adapt_light(light, context, transition)
+ await self._adapt_light(light, context, transition, force=force)
async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None:
assert not self.manager.is_proactively_adapting(event.context.id)
diff --git a/tests/test_adaptation_utils.py b/tests/test_adaptation_utils.py
index fc0df739..20db5037 100644
--- a/tests/test_adaptation_utils.py
+++ b/tests/test_adaptation_utils.py
@@ -312,6 +312,7 @@ async def test_prepare_adaptation_data(
service_data,
split,
filter_by_state,
+ force=False,
)
generated_service_datas = [item async for item in data.service_call_datas]
diff --git a/tests/test_switch.py b/tests/test_switch.py
index c7220ee4..4963209b 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -1342,6 +1342,7 @@ async def test_cancellable_service_calls_task(hass):
context,
0,
_create_service_call_data_iterator(hass, [service_data], False),
+ force=False,
max_length=1,
which="both",
)
From b80b253fa1b840c63415d67b8a6df107cf7bbde3 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 2 Aug 2023 23:12:13 -0700
Subject: [PATCH 090/549] Warning when calling light.turn_on with brightness=0
(#678)
* Do not update the call when calling light.turn_on with brightness=0
* Simplify
* issue warning instead
---
custom_components/adaptive_lighting/switch.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 641ffab6..fd0fb9d1 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1993,6 +1993,13 @@ class AdaptiveLightingManager:
if self.manual_control.get(entity_id, False):
return
+ if data.get(ATTR_BRIGHTNESS) == 0:
+ _LOGGER.warning(
+ "Turn-on call with zero brightness detected, Adaptive Lighting"
+ " intercepted this service_call and adjusted it. If you use this as"
+ " a brightness workaround, please remove it, it is no longer necessary",
+ )
+
try:
adaptive_switch = _switch_with_lights(self.hass, [entity_id])
except NoSwitchFoundError:
From 4251ccacc57552279e346b32e9e6d1ce6c208c7d Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 3 Aug 2023 17:46:19 -0700
Subject: [PATCH 091/549] Bump to 1.18.3 in manifest.json (#698)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 8d6a80b9..7806e04a 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.18.2"
+ "version": "1.18.3"
}
From 1ef7ed507ee25a70a17dc83c5d58d1360da64447 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 3 Aug 2023 17:47:09 -0700
Subject: [PATCH 092/549] Implement call intercept for multiple lights (#679)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Implement call intercept for multiple lights
* remove comment
* skip if no eids
* add comment
* fix type
* Fix skipped
* indentation
* Add logging and fix error
* Fix for HA ≤2023.04
* simplify
* remove unused ignores
* Debug mode
* Add test
* Make test failing
* rename switch
* rename lights
* Fix tests
* Rename lights in tests
* Remove unused dependencies
* Improve tests
* More tests
* Remove the DEBUG_MODE
* Add doc-string
* Extra test
* assert
* extra test
* Comments
* fix
* fix
* expand light groups
* more logging
* sort
* Revert is_proactively_adapting checks
This reverts commit 39fd8f2be0f3d6bef27705d167b5eab8113d5709.
* simplify the mapping
* Revert "Revert is_proactively_adapting checks"
This reverts commit 18803e8e507c9b44934e15320c1bbed7fa30b788.
* test
* no light groups
* do not expand
* Do not expand_light_groups in intercept
* more logging
* Fix
* add comment
* Add multi_light_intercept config option
* Update README.md, strings.json, and services.yaml
* add light group
* fix platform
* add simple test
* turn off again
* Test without take over control
* improve test and fix it in one way
* Fixes
* add cleanup fixture
* format
* Update test_switch.py
* add __str__
* remove unneeded call
* simplify service_data construction
* Generalize is_our_context
* Fix multi_light_intercept: false
* add comments
* add docs
* Update README.md, strings.json, and services.yaml
* Add feature line
* move function
---------
Co-authored-by: github-actions[bot]
---
.ruff.toml | 2 +-
README.md | 6 +-
.../adaptive_lighting/adaptation_utils.py | 14 +
custom_components/adaptive_lighting/const.py | 13 +-
.../adaptive_lighting/strings.json | 3 +-
custom_components/adaptive_lighting/switch.py | 352 ++++++++++++----
.../adaptive_lighting/translations/en.json | 3 +-
tests/test_switch.py | 377 ++++++++++++++++--
8 files changed, 648 insertions(+), 122 deletions(-)
diff --git a/.ruff.toml b/.ruff.toml
index c1b6b784..1cfeeb74 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -16,7 +16,7 @@ ignore = [
"FBT002", # Boolean default value in function definition
"FIX004", # Line contains HACK, consider resolving the issue
"PD901", # df is a bad variable name. Be kinder to your future self.
- "PERF203",# `try`-`except` within a loop incurs performance overhead
+ "PERF203", # `try`-`except` within a loop incurs performance overhead
"PLR0913", # Too many arguments to function call (N > 5)
"PLR2004", # Magic value used in comparison, consider replacing X with a constant variable
"S101", # Use of assert detected
diff --git a/README.md b/README.md
index 9a70b89a..d48ee10d 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,9 @@ In addition to its regular mode, Adaptive Lighting also offers a "sleep mode"
## :bulb: Features
+When initially turning on a light that is controlled by Adaptive Lighting, the `light.turn_on` service call is intercepted, and the light's brightness and color are automatically adjusted based on the sun's position.
+After that, the light's brightness and color are automatically adjusted at a regular interval.
+
Adaptive Lighting provides four switches (using "living_room" as an example component name):
- `switch.adaptive_lighting_living_room`: Turn Adaptive Lighting on or off and view current light settings through its attributes.
@@ -124,7 +127,8 @@ The YAML and frontend configuration methods support all of the options listed be
| `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 |
-| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
+| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
+| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` |
diff --git a/custom_components/adaptive_lighting/adaptation_utils.py b/custom_components/adaptive_lighting/adaptation_utils.py
index 97914327..593a746b 100644
--- a/custom_components/adaptive_lighting/adaptation_utils.py
+++ b/custom_components/adaptive_lighting/adaptation_utils.py
@@ -150,6 +150,20 @@ class AdaptationData:
"""Return data for the next service call, or none if no more data exists."""
return await anext(self.service_call_datas, None)
+ def __str__(self) -> str:
+ """Return a string representation of the data."""
+ return (
+ f"{self.__class__.__name__}("
+ f"entity_id={self.entity_id}, "
+ f"context_id={self.context.id}, "
+ f"sleep_time={self.sleep_time}, "
+ f"force={self.force}, "
+ f"max_length={self.max_length}, "
+ f"which={self.which}, "
+ f"initial_sleep={self.initial_sleep}"
+ ")"
+ )
+
class NoColorOrBrightnessInServiceDataError(Exception):
"""Exception raised when no color or brightness attributes are found in service data."""
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index 94e9a50b..d2b1dc1e 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -186,10 +186,20 @@ CONF_SKIP_REDUNDANT_COMMANDS, DEFAULT_SKIP_REDUNDANT_COMMANDS = (
DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = (
"Skip sending adaptation commands whose target state already "
"equals the light's known state. Minimizes network traffic and improves the "
- "adaptation responsivity in some situations. "
+ "adaptation responsivity in some situations. 📉"
"Disable if physical light states get out of sync with HA's recorded state."
)
+CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = (
+ "multi_light_intercept",
+ True,
+)
+DOCS[CONF_MULTI_LIGHT_INTERCEPT] = (
+ "Intercept and adapt `light.turn_on` calls that target multiple lights. ➗"
+ "⚠️ This might result in splitting up a single `light.turn_on` call "
+ "into multiple calls, e.g., when lights are in different switches."
+)
+
SLEEP_MODE_SWITCH = "sleep_mode_switch"
ADAPT_COLOR_SWITCH = "adapt_color_switch"
ADAPT_BRIGHTNESS_SWITCH = "adapt_brightness_switch"
@@ -290,6 +300,7 @@ VALIDATION_TUPLES = [
DEFAULT_SKIP_REDUNDANT_COMMANDS,
bool,
),
+ (CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool),
]
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index b6d62b0b..f60297c5 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -48,7 +48,8 @@
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
- "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state."
+ "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.",
+ "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches."
}
}
},
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index fd0fb9d1..defe3b85 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -20,6 +20,7 @@ import ulid_transform
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
+ ATTR_COLOR_TEMP,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_FLASH,
@@ -119,6 +120,7 @@ from .const import (
CONF_MIN_BRIGHTNESS,
CONF_MIN_COLOR_TEMP,
CONF_MIN_SUNSET_TIME,
+ CONF_MULTI_LIGHT_INTERCEPT,
CONF_ONLY_ONCE,
CONF_PREFER_RGB_COLOR,
CONF_SEND_SPLIT_DELAY,
@@ -265,30 +267,39 @@ def create_context(
return Context(id=context_id, parent_id=parent_id)
-def is_our_context_id(context_id: str | None) -> bool:
+def is_our_context_id(context_id: str | None, which: str | None = None) -> bool:
"""Check whether this integration created 'context_id'."""
if context_id is None:
return False
- return f":{_DOMAIN_SHORT}:" in context_id
+
+ is_al = f":{_DOMAIN_SHORT}:" in context_id
+ if not is_al:
+ return False
+ if which is None:
+ return True
+ return f":{_remove_vowels(which)}:" in context_id
-def is_our_context(context: Context | None) -> bool:
+def is_our_context(context: Context | None, which: str | None = None) -> bool:
"""Check whether this integration created 'context'."""
if context is None:
return False
- return is_our_context_id(context.id)
+ return is_our_context_id(context.id, which)
@bind_hass
def _switches_with_lights(
hass: HomeAssistant,
lights: list[str],
+ expand_light_groups: bool = True,
) -> list[AdaptiveSwitch]:
"""Get all switches that control at least one of the lights passed."""
config_entries = hass.config_entries.async_entries(DOMAIN)
data = hass.data[DOMAIN]
switches = []
- all_check_lights = _expand_light_groups(hass, lights)
+ all_check_lights = (
+ _expand_light_groups(hass, lights) if expand_light_groups else set(lights)
+ )
for config in config_entries:
entry = data.get(config.entry_id)
if entry is None: # entry might be disabled and therefore missing
@@ -309,9 +320,10 @@ class NoSwitchFoundError(ValueError):
def _switch_with_lights(
hass: HomeAssistant,
lights: list[str],
+ expand_light_groups: bool = True,
) -> AdaptiveSwitch:
"""Find the switch that controls the lights in 'lights'."""
- switches = _switches_with_lights(hass, lights)
+ switches = _switches_with_lights(hass, lights, expand_light_groups)
if len(switches) == 1:
return switches[0]
if len(switches) > 1:
@@ -643,7 +655,10 @@ def _is_state_event(event: Event, from_or_to_state: Iterable[str]):
@bind_hass
-def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
+def _expand_light_groups(
+ hass: HomeAssistant,
+ lights: list[str],
+) -> list[str]:
all_lights = set()
manager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER]
for light in lights:
@@ -651,14 +666,18 @@ def _expand_light_groups(hass: HomeAssistant, lights: list[str]) -> list[str]:
if state is None:
_LOGGER.debug("State of %s is None", light)
all_lights.add(light)
- elif "entity_id" in state.attributes: # it's a light group
+ elif _is_light_group(state):
group = state.attributes["entity_id"]
manager.lights.discard(light)
all_lights.update(group)
_LOGGER.debug("Expanded %s to %s", light, group)
else:
all_lights.add(light)
- return list(all_lights)
+ return sorted(all_lights)
+
+
+def _is_light_group(state: State) -> bool:
+ return "entity_id" in state.attributes
@bind_hass
@@ -927,6 +946,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._take_over_control = True
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS]
+ self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT]
self._expand_light_groups() # updates manual control timers
location, _ = get_astral_location(self.hass)
@@ -1272,11 +1292,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
prefer_rgb_color: bool | None = None,
force: bool = False,
) -> None:
- # This should never happen if it's been proactively adapted.
- # The context.parent_id is the context.id of the service call that was intercepted
- # and context.id here is from the resulting "light_event" event.
- assert not self.manager.is_proactively_adapting(context.parent_id)
-
if (lock := self.manager.turn_off_locks.get(light)) and lock.locked():
_LOGGER.debug("%s: '%s' is locked", self._name, light)
return
@@ -1874,6 +1889,9 @@ class AdaptiveLightingManager:
# Track light transitions
self.transition_timers: dict[str, _AsyncSingleShotTimer] = {}
+ # Track _execute_cancellable_adaptation_calls tasks
+ self.adaptation_tasks = set()
+
# Setup listeners and its callbacks to remove them later
self.listener_removers = [
self.hass.bus.async_listen(
@@ -1958,65 +1976,235 @@ class AdaptiveLightingManager:
for key in keys:
self._proactively_adapting_contexts.pop(key)
- async def _service_interceptor_turn_on_handler( # noqa: PLR0911
+ async def _service_interceptor_turn_on_handler( # noqa: PLR0912, PLR0915
self,
call: ServiceCall,
data: ServiceData,
- ):
- # Don't adapt our own service calls
- if is_our_context(call.context):
+ ) -> None:
+ """Intercept `light.turn_on` and `light.toggle` service calls and adapt them.
+
+ It is possible that the calls are made for multiple lights at once,
+ which in turn might be in different switches or no switches at all.
+ If there are lights that are not all in a single switch, we need to
+ make multiple calls to `light.turn_on` with the correct entity IDs.
+ One of these calls can be intercepted and adapted, the others need to
+ be adapted by calling `_adapt_light` with the correct entity IDs or
+ by calling `light.turn_on` directly.
+
+ We create a mapping from switch to entity IDs and keep a list
+ of skipped lights which are lights in no switches or in switches that
+ are off or lights that are already on.
+
+ If there is only one switch and 0 skipped lights, we just intercept the
+ call directly.
+
+ If there are multiple switches and skipped lights, we can adapt the call
+ for one of the switches to include only the lights in that switch and
+ need to call `_adapt_light` for the other switches with their
+ entity_ids. For skipped lights, we call light.turn_on directly with the
+ entity_ids and original service data.
+
+ If there are only skipped lights, we can use the intercepted call
+ directly.
+ """
+ is_skipped_hash = is_our_context(call.context, "skipped")
+ _LOGGER.debug(
+ "(0) _service_interceptor_turn_on_handler: call.context.id='%s', is_skipped_hash='%s'",
+ call.context.id,
+ is_skipped_hash,
+ )
+ if is_our_context(call.context) and not is_skipped_hash:
+ # Don't adapt our own service calls, but do re-adapt calls that
+ # were skipped by us
return
if ATTR_EFFECT in data[CONF_PARAMS] or ATTR_FLASH in data[CONF_PARAMS]:
return
+ _LOGGER.debug(
+ "(1) _service_interceptor_turn_on_handler: call='%s', data='%s'",
+ call,
+ data,
+ )
+
entity_ids = self._get_entity_list(data)
+ # Note: we do not expand light groups anywhere in this method, instead
+ # we skip them and rely on the followup call that HA will make
+ # with the expanded entity IDs.
- # For simplicity, only service calls affecting a single entity are currently handled.
- #
- # To add support for adapting multiple entities, the following properties
- # need to hold for _all_ entities:
- # - managed by this AL instance
- # - not manually controlled
- # - supporting the same relevant feature set
- # - off state
- if len(entity_ids) != 1:
- return
+ # Create a mapping from switch to entity IDs
+ # AdaptiveSwitch.name → entity_ids mapping
+ switch_to_eids: dict[str, list[str]] = {}
+ # AdaptiveSwitch.name → AdaptiveSwitch mapping
+ switch_name_mapping: dict[str, AdaptiveSwitch] = {}
+ # Note: In HA≥2023.5, AdaptiveSwitch is hashable, so we can
+ # use dict[AdaptiveSwitch, list[str]]
+ skipped: list[str] = []
+ for entity_id in entity_ids:
+ try:
+ switch = _switch_with_lights(
+ self.hass,
+ [entity_id],
+ # Do not expand light groups, because HA will make a separate light.turn_on
+ # call where the lights are expanded, and that call will be intercepted.
+ expand_light_groups=False,
+ )
+ except NoSwitchFoundError:
+ # Needs to make the original call but without adaptation
+ skipped.append(entity_id)
+ _LOGGER.debug(
+ "No switch found for entity_id='%s', skipped='%s'",
+ entity_id,
+ skipped,
+ )
+ else:
+ if (
+ not switch.is_on
+ # Never adapt on light groups, because HA will make a separate light.turn_on
+ or _is_light_group(self.hass.states.get(entity_id))
+ # Prevent adaptation of TURN_ON calls when light is already on,
+ # and of TOGGLE calls when toggling off.
+ or self.hass.states.is_state(entity_id, STATE_ON)
+ or self.manual_control.get(entity_id, False)
+ ):
+ _LOGGER.debug(
+ "Switch is off or light is already on for entity_id='%s', skipped='%s'"
+ " (is_on='%s', is_state='%s', manual_control='%s')",
+ entity_id,
+ skipped,
+ switch.is_on,
+ self.hass.states.is_state(entity_id, STATE_ON),
+ self.manual_control.get(entity_id, False),
+ )
+ skipped.append(entity_id)
+ else:
+ switch_to_eids.setdefault(switch.name, []).append(entity_id)
+ switch_name_mapping[switch.name] = switch
- entity_id = entity_ids[0]
-
- # Prevent adaptation of TURN_ON calls when light is already on,
- # and of TOGGLE calls when toggling off.
- if self.hass.states.is_state(entity_id, STATE_ON):
- return
-
- if self.manual_control.get(entity_id, False):
- return
-
- if data.get(ATTR_BRIGHTNESS) == 0:
+ # Check for `multi_light_intercept: true/false`
+ mli = [sw._multi_light_intercept for sw in switch_name_mapping.values()]
+ more_than_one_switch = len(switch_to_eids) > 1
+ single_switch_with_multiple_lights = (
+ len(switch_to_eids) == 1 and len(next(iter(switch_to_eids.values()))) > 1
+ )
+ switch_without_multi_light_intercept = not all(mli)
+ if more_than_one_switch and switch_without_multi_light_intercept:
_LOGGER.warning(
- "Turn-on call with zero brightness detected, Adaptive Lighting"
- " intercepted this service_call and adjusted it. If you use this as"
- " a brightness workaround, please remove it, it is no longer necessary",
+ "Multiple switches (%s) targeted, but not all have"
+ " `multi_light_intercept: true`, so skipping intercept"
+ " for all lights.",
+ switch_to_eids,
)
+ skipped = entity_ids
+ switch_to_eids = {}
+ elif (
+ single_switch_with_multiple_lights and switch_without_multi_light_intercept
+ ):
+ _LOGGER.warning(
+ "Single switch with multiple lights targeted, but"
+ " `multi_light_intercept: true` is not set, so skipping intercept"
+ " for all lights.",
+ switch_to_eids,
+ )
+ skipped = entity_ids
+ switch_to_eids = {}
- try:
- adaptive_switch = _switch_with_lights(self.hass, [entity_id])
- except NoSwitchFoundError:
- # This might be a light that is not managed by this AL instance.
+ _LOGGER.debug(
+ "(2) _service_interceptor_turn_on_handler: switch_to_eids='%s', skipped='%s'",
+ switch_to_eids,
+ skipped,
+ )
+
+ def modify_service_data(service_data, entity_ids):
+ """Modify the service data to contain the entity IDs."""
+ service_data.pop(ATTR_ENTITY_ID, None)
+ service_data.pop(ATTR_AREA_ID, None)
+ service_data[ATTR_ENTITY_ID] = entity_ids
+ return service_data
+
+ # Intercept the call for first switch and call _adapt_light for the rest
+ has_intercepted = False # Can only intercept a turn_on call once
+ for adaptive_switch_name, _entity_ids in switch_to_eids.items():
+ switch = switch_name_mapping[adaptive_switch_name]
+ transition = data[CONF_PARAMS].get(
+ ATTR_TRANSITION,
+ switch.initial_transition,
+ )
+ if not has_intercepted:
+ _LOGGER.debug(
+ "(3) _service_interceptor_turn_on_handler: intercepting entity_ids='%s'",
+ _entity_ids,
+ )
+ await self._service_interceptor_turn_on_single_light_handler(
+ entity_ids=_entity_ids,
+ switch=switch,
+ transition=transition,
+ call=call,
+ data=modify_service_data(data, _entity_ids),
+ )
+ has_intercepted = True
+ continue
+
+ for eid in _entity_ids:
+ # Must add a new context otherwise _adapt_light will bail out
+ context = switch.create_context("intercept")
+ self.clear_proactively_adapting(eid)
+ self.set_proactively_adapting(context.id, eid)
+ _LOGGER.debug(
+ "(4) _service_interceptor_turn_on_handler: calling `_adapt_light` with eid='%s', context='%s', transition='%s'",
+ eid,
+ context,
+ transition,
+ )
+ await switch._adapt_light(
+ light=eid,
+ context=context,
+ transition=transition,
+ )
+
+ # Call light.turn_on service for skipped entities
+ if skipped:
+ if not has_intercepted:
+ assert set(skipped) == set(entity_ids)
+ return # The call will be intercepted with the original data
+ # Call light turn_on service for skipped entities
+ context = switch.create_context("skipped")
_LOGGER.debug(
- "No (or multiple) adaptive switch(es) found for entity %s,"
- " skipping adaptation by intercepting service call",
- entity_id,
+ "(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'",
+ skipped,
+ data,
+ context.id,
+ )
+ # Need to expand light groups here because otherwise this interceptor loop will happen twice more
+ _LOGGER.debug(
+ "(6) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'",
+ skipped,
+ data,
+ context.id,
+ )
+ service_data = {ATTR_ENTITY_ID: skipped, **data[CONF_PARAMS]}
+ if (
+ ATTR_COLOR_TEMP in service_data
+ and ATTR_COLOR_TEMP_KELVIN in service_data
+ ):
+ # ATTR_COLOR_TEMP and ATTR_COLOR_TEMP_KELVIN are mutually exclusive
+ del service_data[ATTR_COLOR_TEMP]
+ await self.hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ service_data,
+ blocking=True,
+ context=context,
)
- return
-
- if not adaptive_switch.is_on:
- return
-
- if entity_id not in adaptive_switch.lights:
- return
+ async def _service_interceptor_turn_on_single_light_handler(
+ self,
+ entity_ids: list[str],
+ switch: AdaptiveSwitch,
+ transition: int,
+ call: ServiceCall,
+ data: ServiceData,
+ ):
_LOGGER.debug(
"Intercepted TURN_ON call with data %s (%s)",
data,
@@ -2024,18 +2212,13 @@ class AdaptiveLightingManager:
)
# Reset because turning on the light, this also happens in
- # `turn_on_off_event_listener`, however, this function is called
+ # `state_changed_event_listener`, however, this function is called
# before that one.
- self.reset(entity_id, reset_manual_control=False)
+ self.reset(*entity_ids, reset_manual_control=False)
+ for entity_id in entity_ids:
+ self.clear_proactively_adapting(entity_id)
- self.clear_proactively_adapting(entity_id)
-
- transition = data[CONF_PARAMS].get(
- ATTR_TRANSITION,
- adaptive_switch.initial_transition,
- )
-
- adaptation_data = await adaptive_switch.prepare_adaptation_data(
+ adaptation_data = await switch.prepare_adaptation_data(
entity_id,
transition,
)
@@ -2063,12 +2246,21 @@ class AdaptiveLightingManager:
# We cannot know here whether there is another call to follow (since the
# state can change until the next call), so we just schedule it and let
# it sort out by itself.
- self.set_proactively_adapting(call.context.id, entity_id)
- self.set_proactively_adapting(adaptation_data.context.id, entity_id)
+ for entity_id in entity_ids:
+ self.set_proactively_adapting(call.context.id, entity_id)
+ self.set_proactively_adapting(adaptation_data.context.id, entity_id)
adaptation_data.initial_sleep = True
- _ = asyncio.create_task( # Don't await to avoid blocking the service call
- adaptive_switch.execute_cancellable_adaptation_calls(adaptation_data),
+
+ # Don't await to avoid blocking the service call.
+ # Assign to a variable only to await in tests.
+ self.adaptation_tasks.add(
+ asyncio.create_task(
+ switch.execute_cancellable_adaptation_calls(adaptation_data),
+ ),
)
+ # Remove tasks that are done
+ if done_tasks := [t for t in self.adaptation_tasks if t.done()]:
+ self.adaptation_tasks.difference_update(done_tasks)
def _handle_timer(
self,
@@ -2092,14 +2284,22 @@ class AdaptiveLightingManager:
def start_transition_timer(self, light: str) -> None:
"""Mark a light as manually controlled."""
- last_service_data = self.last_service_data[light]
- last_transition = last_service_data.get(ATTR_TRANSITION)
- if not last_transition:
+ last_service_data = self.last_service_data.get(light)
+ if last_service_data is None:
_LOGGER.debug(
- "No transition in last adapt for light %s, continuing...",
+ "No last service data for light %s, not starting timer.",
light,
)
return
+
+ last_transition = last_service_data.get(ATTR_TRANSITION)
+ if not last_transition:
+ _LOGGER.debug(
+ "No transition in last adapt for light %s, not starting timer.",
+ light,
+ )
+ return
+
_LOGGER.debug(
"Start transition timer of %s seconds for light %s",
last_transition,
@@ -2198,8 +2398,7 @@ class AdaptiveLightingManager:
for light in lights:
if reset_manual_control:
self.manual_control[light] = False
- timer = self.auto_reset_manual_control_timers.pop(light, None)
- if timer is not None:
+ if timer := self.auto_reset_manual_control_timers.pop(light, None):
timer.cancel()
self.our_last_state_on_change.pop(light, None)
self.last_service_data.pop(light, None)
@@ -2380,6 +2579,7 @@ class AdaptiveLightingManager:
entity_id,
event.context.id,
)
+ # Note: the reset below already happened in `_service_interceptor_turn_on_handler`
return
self.reset(entity_id, reset_manual_control=False)
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index 358f7cbe..7e58be17 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -49,7 +49,8 @@
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
- "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. Disable if physical light states get out of sync with HA's recorded state."
+ "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.",
+ "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches."
}
}
},
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 4963209b..e3bc9126 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -1,6 +1,7 @@
"""Tests for Adaptive Lighting switches."""
# pylint: disable=protected-access
import asyncio
+import itertools
from copy import deepcopy
import datetime
import logging
@@ -55,6 +56,7 @@ from custom_components.adaptive_lighting.adaptation_utils import (
from custom_components.adaptive_lighting.const import (
ADAPT_BRIGHTNESS_SWITCH,
ADAPT_COLOR_SWITCH,
+ CONF_TAKE_OVER_CONTROL,
ATTR_ADAPTIVE_LIGHTING_MANAGER,
CONF_ADAPT_UNTIL_SLEEP,
CONF_AUTORESET_CONTROL,
@@ -64,6 +66,7 @@ from custom_components.adaptive_lighting.const import (
CONF_MAX_BRIGHTNESS,
CONF_MIN_COLOR_TEMP,
CONF_PREFER_RGB_COLOR,
+ CONF_MULTI_LIGHT_INTERCEPT,
CONF_SEPARATE_TURN_ON_COMMANDS,
CONF_SLEEP_RGB_OR_COLOR_TEMP,
CONF_SUNRISE_OFFSET,
@@ -90,6 +93,7 @@ from custom_components.adaptive_lighting.switch import (
_attributes_have_changed,
color_difference_redmean,
create_context,
+ AdaptiveLightingManager,
is_our_context,
is_our_context_id,
lerp_color_hsv,
@@ -141,6 +145,18 @@ def reset_time_zone():
dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE
+@pytest.fixture
+async def cleanup(hass):
+ yield
+ manager: AdaptiveLightingManager = hass.data[DOMAIN][ATTR_ADAPTIVE_LIGHTING_MANAGER]
+ for timer in manager.auto_reset_manual_control_timers.values():
+ timer.cancel()
+ for timer in manager.transition_timers.values():
+ timer.cancel()
+ for task in manager.adaptation_tasks:
+ task.cancel()
+
+
async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitch]:
"""Create the switch entry."""
entry = MockConfigEntry(
@@ -159,51 +175,46 @@ async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitc
return entry, switch
-async def setup_lights(hass: HomeAssistant):
+async def setup_lights(hass: HomeAssistant, with_group: bool = False):
"""Set up 3 light entities using the 'template' platform."""
+ n = 3 if not with_group else 5 # last 2 will be put in a group
+ template_lights = {
+ f"light_{i}": {
+ "unique_id": f"light_{i}",
+ "friendly_name": f"light_{i}",
+ "turn_on": None,
+ "turn_off": None,
+ "set_level": None,
+ "set_temperature": None,
+ "set_color": None,
+ }
+ for i in range(1, n + 1)
+ }
+ template_lights["light_3"]["supports_transition_template"] = True
+ platforms = [{"platform": "template", "lights": template_lights}]
+
+ if with_group:
+ platforms.append(
+ {
+ "platform": "group",
+ "entities": ["light.light_4", "light.light_5"],
+ "name": "Light Group",
+ "unique_id": "light_group",
+ "all": "false",
+ }
+ )
+
await async_setup_component(
hass,
LIGHT_DOMAIN,
- {
- LIGHT_DOMAIN: [
- {
- "platform": "template",
- "lights": {
- "light_1": {
- "friendly_name": "light_1",
- "unique_id": "light_1",
- "turn_on": None,
- "turn_off": None,
- "set_level": None,
- "set_temperature": None,
- "set_color": None,
- },
- "light_2": {
- "friendly_name": "light_2",
- "unique_id": "light_2",
- "turn_on": None,
- "turn_off": None,
- "set_level": None,
- "set_temperature": None,
- "set_color": None,
- },
- "light_3": {
- "friendly_name": "light_3",
- "unique_id": "light_3",
- "turn_on": None,
- "turn_off": None,
- "set_level": None,
- "set_temperature": None,
- "set_color": None,
- "supports_transition_template": True,
- },
- },
- },
- ]
- },
+ {LIGHT_DOMAIN: platforms},
)
-
await hass.async_block_till_done()
+
+ if with_group:
+ state = hass.states.get("light.light_group")
+ assert state.attributes["entity_id"] == ["light.light_4", "light.light_5"]
+
platform = async_get_platforms(hass, "template")
lights = list(platform[0].entities.values())
@@ -1374,13 +1385,15 @@ async def test_service_calls_task_cancellation(hass):
async def _turn_on_and_track_event_contexts(
- hass: HomeAssistant, context_id: str, entity_id
+ hass: HomeAssistant, context_id: str, entity_id, return_full_events: bool = False
):
context = Context(id=context_id)
event_context_ids = []
+ events = []
async def turn_on_off_event_listener(event: Event) -> None:
event_context_ids.append(event.context.id)
+ events.append(event)
hass.bus.async_listen(EVENT_CALL_SERVICE, turn_on_off_event_listener)
@@ -1392,7 +1405,8 @@ async def _turn_on_and_track_event_contexts(
context=context,
)
await hass.async_block_till_done()
-
+ if return_full_events:
+ return events
return event_context_ids
@@ -1540,6 +1554,148 @@ async def test_proactive_adaptation_transition_override(hass):
switch.manager.cancel_ongoing_adaptation_calls(ENTITY_LIGHT_3)
+async def setup_proactive_multiple_lights_two_switches(hass):
+ lights_instances = await setup_lights(hass)
+ # Setup switches
+ lights = [
+ ENTITY_LIGHT_1,
+ ENTITY_LIGHT_2,
+ ENTITY_LIGHT_3,
+ ]
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: lights},
+ blocking=True,
+ )
+ defaults = {
+ CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour),
+ CONF_SUNSET_TIME: datetime.time(SUNSET.hour),
+ CONF_INITIAL_TRANSITION: 0,
+ CONF_TRANSITION: 0,
+ CONF_DETECT_NON_HA_CHANGES: True,
+ CONF_PREFER_RGB_COLOR: False,
+ CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp}
+ INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True,
+ }
+ _, switch1 = await setup_switch(
+ hass, {CONF_NAME: "switch1", CONF_LIGHTS: [ENTITY_LIGHT_1], **defaults}
+ )
+ _, switch2 = await setup_switch(
+ hass, {CONF_NAME: "switch2", CONF_LIGHTS: [ENTITY_LIGHT_2], **defaults}
+ )
+ assert hass.states.get(switch1.entity_id).state == STATE_ON
+ assert hass.states.get(switch2.entity_id).state == STATE_ON
+ assert all(hass.states.get(light).state == STATE_OFF for light in lights)
+ return lights, switch1, switch2
+
+
+async def test_proactive_multiple_lights_all_at_once(hass):
+ """Create switch and demo lights."""
+ lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass)
+ _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once")
+ # Setup demo lights and turn on
+ events = await _turn_on_and_track_event_contexts(
+ hass, "test1", lights, return_full_events=True
+ )
+ assert len(events) == 3, events
+
+ # Original turn_on call that is intercepted
+ assert events[0].context.id == "test1"
+ assert events[0].data["service_data"][ATTR_ENTITY_ID] == lights
+
+ # The `has_intercepted` path
+ assert events[1].data["service_data"][ATTR_ENTITY_ID] == ENTITY_LIGHT_2
+ assert ":ntrc:" in events[1].context.id
+
+ # The skipped lights, the one not in a switch
+ assert events[2].data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3]
+ assert ":skpp:" in events[2].context.id
+
+ assert switch1.manager.is_proactively_adapting("test1")
+ assert switch2.manager.is_proactively_adapting("test1")
+
+ await hass.async_block_till_done()
+
+ assert all(hass.states.get(light).state == STATE_ON for light in lights)
+
+ # Turn on second time even though already on
+ events = await _turn_on_and_track_event_contexts(
+ hass, "test2", lights, return_full_events=True
+ )
+ assert len(events) == 1, events
+ assert events[0].context.id == "test2"
+
+
+async def test_proactive_multiple_lights_turn_on_non_managed_light(hass):
+ """Create switch and demo lights."""
+ lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass)
+ turn_ons = await _turn_on_and_track_event_contexts(hass, "test1", lights)
+ assert len(turn_ons) == 3, turn_ons
+ await hass.async_block_till_done()
+ assert all(hass.states.get(light).state == STATE_ON for light in lights)
+
+ # Turn off ENTITY_LIGHT_3 (which is not in a switch), leaving the other two on
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: ENTITY_LIGHT_3},
+ blocking=True,
+ context=Context(id="test2"),
+ )
+
+ # Now turn on all lights again, which means the code gets to "if skipped: if not has_intercepted:"
+ turn_ons = await _turn_on_and_track_event_contexts(hass, "test2", ENTITY_LIGHT_3)
+ assert len(turn_ons) == 1, turn_ons
+
+
+async def test_proactive_multiple_lights_turn_on_managed_lights_only(hass):
+ """Create switch and demo lights."""
+ lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass)
+ _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once")
+ # Setup demo lights and turn on
+ events = await _turn_on_and_track_event_contexts(
+ hass, "test1", lights[:-1], return_full_events=True
+ )
+ assert len(events) == 2, events
+
+ # Original turn_on call that is intercepted
+ assert events[0].context.id == "test1"
+ assert events[0].data["service_data"][ATTR_ENTITY_ID] == lights[:-1]
+
+ # The `has_intercepted` path
+ assert events[1].data["service_data"][ATTR_ENTITY_ID] == ENTITY_LIGHT_2
+ assert ":ntrc:" in events[1].context.id
+ assert ATTR_BRIGHTNESS in events[1].data["service_data"]
+
+
+async def test_proactive_multiple_lights_one_switch_and_one_skipped(hass):
+ """Create switch and demo lights."""
+ lights, switch1, switch2 = await setup_proactive_multiple_lights_two_switches(hass)
+ two_lights = [lights[0], lights[-1]]
+ _LOGGER.debug("Start test_proactive_multiple_lights_all_at_once")
+ # Setup demo lights and turn on
+ events = await _turn_on_and_track_event_contexts(
+ hass, "test1", two_lights, return_full_events=True
+ )
+ assert len(events) == 2, events
+
+ # Original turn_on call that is intercepted
+ assert events[0].context.id == "test1"
+ assert events[0].data["service_data"][ATTR_ENTITY_ID] == two_lights
+
+ # The skipped lights, the one not in a switch
+ assert events[1].data["service_data"][ATTR_ENTITY_ID] == [ENTITY_LIGHT_3]
+ assert ":skpp:" in events[1].context.id
+
+ assert switch1.manager.is_proactively_adapting("test1")
+ assert switch2.manager.is_proactively_adapting("test1")
+
+ await hass.async_block_till_done()
+
+ assert all(hass.states.get(light).state == STATE_ON for light in two_lights)
+
+
async def test_two_switches_for_single_light(hass):
"""Test the case where someone has two switches for a single light.
@@ -1697,3 +1853,142 @@ def test_lerp_color_hsv():
with pytest.raises(AssertionError):
lerp_color_hsv((255, 0, 0), (0, 255, 0), 1.1)
+
+
+@pytest.mark.parametrize("proactive_service_call_adaptation", [True, False])
+@pytest.mark.parametrize("take_over_control", [True, False])
+@pytest.mark.parametrize("multi_light_intercept", [True, False])
+async def test_light_group(
+ hass,
+ proactive_service_call_adaptation,
+ take_over_control,
+ multi_light_intercept,
+ cleanup,
+):
+ lights = await setup_lights(hass, with_group=True)
+ all_entity_ids = [light.entity_id for light in lights]
+ entity_ids = all_entity_ids[:3] # the last two are in the group
+ entity_ids.append("light.light_group")
+ _, switch = await setup_switch(
+ hass,
+ {
+ CONF_LIGHTS: entity_ids,
+ INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation,
+ CONF_TAKE_OVER_CONTROL: take_over_control,
+ CONF_MULTI_LIGHT_INTERCEPT: multi_light_intercept,
+ },
+ )
+ await hass.async_block_till_done()
+ assert switch.is_on
+ assert all(eid in switch.lights for eid in all_entity_ids)
+
+ # Set the brightness of the group twice, once to turn it on and once to
+ # trigger manual control
+ for _ in range(2):
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS_PCT: 50},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ await switch._update_attrs_and_maybe_adapt_lights(
+ context=switch.create_context("test")
+ )
+ await hass.async_block_till_done()
+
+ if take_over_control:
+ assert switch.manager.manual_control["light.light_4"]
+ assert switch.manager.manual_control["light.light_5"]
+ else:
+ assert not switch.manager.manual_control["light.light_4"]
+ assert not switch.manager.manual_control["light.light_5"]
+
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: "light.light_group"},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ assert not switch.manager.manual_control["light.light_4"]
+ assert not switch.manager.manual_control["light.light_5"]
+ events = await _turn_on_and_track_event_contexts(
+ hass, "testing", "light.light_group", return_full_events=True
+ )
+ if proactive_service_call_adaptation and multi_light_intercept:
+ await asyncio.gather(*switch.manager.adaptation_tasks)
+ # Both lights should be adapted via interception, so with the original context
+ # [
+ # "testing", # original call light 4
+ # "testing", # original call light 5
+ # ]
+
+ assert events[0].data["service_data"][ATTR_ENTITY_ID] == "light.light_group"
+ assert events[0].context.id == "testing"
+ assert events[1].data["service_data"][ATTR_ENTITY_ID] == [
+ "light.light_4",
+ "light.light_5",
+ ]
+ assert events[1].context.id == "testing"
+ else:
+ assert events[0].data["service_data"][ATTR_ENTITY_ID] == "light.light_group"
+ assert events[0].context.id == "testing"
+ assert events[1].data["service_data"][ATTR_ENTITY_ID] == [
+ "light.light_4",
+ "light.light_5",
+ ]
+ assert events[1].context.id == "testing"
+ e1 = events[2].data["service_data"][ATTR_ENTITY_ID]
+ e2 = events[3].data["service_data"][ATTR_ENTITY_ID]
+ assert (
+ e1 == "light.light_4"
+ and e2 == "light.light_5"
+ or e1 == "light.light_5"
+ and e2 == "light.light_4"
+ )
+ assert ":lght:" in events[2].context.id
+ assert ":lght:" in events[3].context.id
+ assert len(events) == 4
+ assert not switch.manager.is_proactively_adapting(events[0].context.id)
+ assert not switch.manager.is_proactively_adapting(events[1].context.id)
+
+ # Turn off all lights, and then turn on all lights
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: all_entity_ids},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+
+ # This turns on light_1, light_2, light_3, light_group (which is light_4 and light_5)
+ # This should result in the intercepted adaptation of light_1, light_2, light_3
+ # and skip the light_group first. Then on a second light.turn_on where the
+ # light_group is expanded, with a :skpp: context_id, this goes trhough another iteration,
+ # and then the light_group is adapted.
+ events = await _turn_on_and_track_event_contexts(
+ hass, "testing", entity_ids, return_full_events=True
+ )
+ if proactive_service_call_adaptation and multi_light_intercept:
+ await asyncio.gather(*switch.manager.adaptation_tasks)
+ # Original call
+ assert events[0].data["service_data"][ATTR_ENTITY_ID] == [
+ "light.light_1",
+ "light.light_2",
+ "light.light_3",
+ "light.light_group",
+ ]
+ assert events[0].context.id == "testing"
+ # Skipped call with light_group
+ assert events[1].data["service_data"][ATTR_ENTITY_ID] == ["light.light_group"]
+ assert ":skpp:" in events[1].context.id
+ # HA automatically forwarded call with light_group expanded with same context
+ assert events[2].data["service_data"][ATTR_ENTITY_ID] == [
+ "light.light_4",
+ "light.light_5",
+ ]
+ assert ":skpp:" in events[2].context.id
+ assert len(events) == 3
From 9a7cff9d6aca269c1db14307684547708bdc3eab Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Fri, 4 Aug 2023 14:45:12 -0700
Subject: [PATCH 093/549] feat: change the order of options in config flow
(#700)
* Change the order of options in config flow
* Update README.md, strings.json, and services.yaml
---------
Co-authored-by: github-actions[bot]
---
README.md | 20 ++++++-------
custom_components/adaptive_lighting/const.py | 29 +++++++++----------
.../adaptive_lighting/services.yaml | 4 +--
.../adaptive_lighting/strings.json | 26 ++++++++---------
.../adaptive_lighting/translations/en.json | 26 ++++++++---------
5 files changed, 52 insertions(+), 53 deletions(-)
diff --git a/README.md b/README.md
index d48ee10d..963c4e9a 100644
--- a/README.md
+++ b/README.md
@@ -99,36 +99,36 @@ The YAML and frontend configuration methods support all of the options listed be
| Variable name | Description | Default | Type |
|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
| `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` |
-| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
-| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
-| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 |
-| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
-| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙 | `False` | `bool` |
| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
+| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
+| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
+| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
+| `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` |
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
-| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅 | `None` | `str` |
+| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` |
| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
-| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇 | `None` | `str` |
+| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` |
| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` |
+| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
+| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
| `send_split_delay` | 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 |
| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` |
+| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index d2b1dc1e..f204e1ac 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -125,7 +125,7 @@ DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅"
CONF_MAX_SUNRISE_TIME = "max_sunrise_time"
DOCS[CONF_MAX_SUNRISE_TIME] = (
"Set the latest virtual sunrise time (HH:MM:SS), allowing"
- " for earlier real sunrises. 🌅"
+ " for earlier sunrises. 🌅"
)
CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET = "sunset_offset", 0
@@ -137,10 +137,9 @@ CONF_SUNSET_TIME = "sunset_time"
DOCS[CONF_SUNSET_TIME] = "Set a fixed time (HH:MM:SS) for sunset. 🌇"
CONF_MIN_SUNSET_TIME = "min_sunset_time"
-DOCS[CONF_MIN_SUNSET_TIME] = (
- "Set the earliest virtual sunset time (HH:MM:SS), allowing"
- " for later real sunsets. 🌇"
-)
+DOCS[
+ CONF_MIN_SUNSET_TIME
+] = "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇"
CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True
DOCS[CONF_TAKE_OVER_CONTROL] = (
@@ -249,17 +248,14 @@ def int_between(min_int, max_int):
VALIDATION_TUPLES = [
(CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids),
- (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool),
- (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool),
- (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
- (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION),
- (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION),
- (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool),
(CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int),
+ (CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION),
+ (CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)),
(CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS, int_between(1, 100)),
(CONF_MIN_COLOR_TEMP, DEFAULT_MIN_COLOR_TEMP, int_between(1000, 10000)),
(CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP, int_between(1000, 10000)),
+ (CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool),
(CONF_SLEEP_BRIGHTNESS, DEFAULT_SLEEP_BRIGHTNESS, int_between(1, 100)),
(
CONF_SLEEP_RGB_OR_COLOR_TEMP,
@@ -278,29 +274,32 @@ VALIDATION_TUPLES = [
DEFAULT_SLEEP_RGB_COLOR,
selector.ColorRGBSelector(selector.ColorRGBSelectorConfig()),
),
+ (CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION),
+ (CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool),
(CONF_SUNRISE_TIME, NONE_STR, str),
(CONF_MAX_SUNRISE_TIME, NONE_STR, str),
(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int),
(CONF_SUNSET_TIME, NONE_STR, str),
(CONF_MIN_SUNSET_TIME, NONE_STR, str),
(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int),
- (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool),
(CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, 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_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float),
(
CONF_AUTORESET_CONTROL,
DEFAULT_AUTORESET_CONTROL,
int_between(0, 365 * 24 * 60 * 60), # 1 year max
),
+ (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, 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_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float),
(
CONF_SKIP_REDUNDANT_COMMANDS,
DEFAULT_SKIP_REDUNDANT_COMMANDS,
bool,
),
(CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool),
+ (CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool),
]
diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml
index 5b857b32..4b79f88c 100644
--- a/custom_components/adaptive_lighting/services.yaml
+++ b/custom_components/adaptive_lighting/services.yaml
@@ -208,13 +208,13 @@ change_switch_settings:
selector:
time: null
max_sunrise_time:
- description: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅
+ description: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅
example: ''
required: false
selector:
time: null
min_sunset_time:
- description: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇
+ description: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇
example: ''
required: false
selector:
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index f60297c5..a942e66b 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -20,36 +20,36 @@
"description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.",
"data": {
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
- "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
- "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
- "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
- "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
- "transition": "transition: Duration of transition when lights change, in seconds. 🕑",
- "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
"interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
+ "transition": "transition: Duration of transition when lights change, in seconds. 🕑",
+ "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
"min_brightness": "min_brightness: Minimum brightness percentage. 💡",
"max_brightness": "max_brightness: Maximum brightness percentage. 💡",
"min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥",
"max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️",
+ "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
"sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴",
"sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
"sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
"sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
+ "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled 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. 🌙",
"sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
- "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
+ "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
"sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
"sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
- "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
+ "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
- "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
+ "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
- "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
"skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.",
- "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches."
+ "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.",
+ "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝"
}
}
},
@@ -204,11 +204,11 @@
"name": "sunset_time"
},
"max_sunrise_time": {
- "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
+ "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
"name": "max_sunrise_time"
},
"min_sunset_time": {
- "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
+ "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
"name": "min_sunset_time"
},
"take_over_control": {
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index 7e58be17..b27ee3c4 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -21,36 +21,36 @@
"description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.",
"data": {
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
- "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
- "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝",
- "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
- "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
- "transition": "transition: Duration of transition when lights change, in seconds. 🕑",
- "transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
"interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
+ "transition": "transition: Duration of transition when lights change, in seconds. 🕑",
+ "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
"min_brightness": "min_brightness: Minimum brightness percentage. 💡",
"max_brightness": "max_brightness: Maximum brightness percentage. 💡",
"min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥",
"max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️",
+ "prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
"sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴",
"sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
"sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
"sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
+ "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled 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. 🌙",
"sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
- "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
+ "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
"sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
"sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
- "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
+ "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
- "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
+ "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
- "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
"skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.",
- "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches."
+ "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.",
+ "include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝"
}
}
},
@@ -205,11 +205,11 @@
"name": "sunset_time"
},
"max_sunrise_time": {
- "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier real sunrises. 🌅",
+ "description": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
"name": "max_sunrise_time"
},
"min_sunset_time": {
- "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later real sunsets. 🌇",
+ "description": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
"name": "min_sunset_time"
},
"take_over_control": {
From a1cec19351426d42a8ba32c87ef5f4791bd6cbfa Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 5 Aug 2023 13:14:21 -0700
Subject: [PATCH 094/549] feat: add different brightness ramping mechanisms
(#699)
* feat: add different brightness ramping mechanisms
* Rephrase
* Fix curves
* update images
* link to other graphs
* update images
---
README.md | 97 ++++---
custom_components/adaptive_lighting/const.py | 38 +++
.../adaptive_lighting/helpers.py | 206 ++++++++++++++
.../adaptive_lighting/strings.json | 5 +-
custom_components/adaptive_lighting/switch.py | 269 +++++++++---------
.../adaptive_lighting/translations/en.json | 5 +-
tests/test_switch.py | 89 +++++-
7 files changed, 534 insertions(+), 175 deletions(-)
create mode 100644 custom_components/adaptive_lighting/helpers.py
diff --git a/README.md b/README.md
index 963c4e9a..71500862 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as
- [:thermometer: Color Temperature](#thermometer-color-temperature)
- [:high_brightness: Brightness](#high_brightness-brightness)
- [While using `transition_until_sleep: true`](#while-using-transition_until_sleep-true)
+ - [Custom brightness ramps using `brightness_mode` with `"linear"` and `"tanh"`](#custom-brightness-ramps-using-brightness_mode-with-linear-and-tanh)
- [:eyes: See also](#eyes-see-also)
- [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors)
@@ -96,39 +97,43 @@ The YAML and frontend configuration methods support all of the options listed be
-| Variable name | Description | Default | Type |
-|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:-------------------------------------|
-| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s |
-| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
-| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
-| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
-| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
-| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
-| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
-| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
-| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
-| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
-| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
-| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
-| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
-| `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` |
-| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
-| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` |
-| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
-| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` |
-| `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` |
-| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` |
-| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
-| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
-| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
-| `send_split_delay` | 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` |
-| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
-| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` |
-| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
+| Variable name | Description | Default | Type |
+|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------|
+| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s |
+| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
+| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
+| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
+| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
+| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
+| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
+| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
+| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
+| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
+| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
+| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
+| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
+| `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` |
+| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
+| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` |
+| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
+| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` |
+| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` |
+| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` |
+| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` |
+| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
+| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
+| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` |
+| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
+| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
+| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
+| `send_split_delay` | 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` |
+| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
+| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` |
+| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
@@ -400,6 +405,32 @@ These graphs were generated using the values calculated by the Adaptive Lighting
### While using `transition_until_sleep: true`

+### Custom brightness ramps using `brightness_mode` with `"linear"` and `"tanh"`
+
+
+Enhance your control over brightness transitions during sunrise and sunset with brightness_mode (click here to learn more 🧠).
+
+With Adaptive Lighting, you can set a `brightness_mode` to specify how the brightness changes during sunrise and sunset. The `brightness_mode` can be set to `"default"` ([as illustrated in other graphs above](#high_brightness-brightness)), `"linear"`, or `"tanh"`. If you choose to deviate from the `"default"` mode, you can adjust `brightness_mode_time_dark` and `brightness_mode_time_light` to further customize the lighting transitions.
+
+When `brightness_mode` is set to `"linear"`:
+
+- During **_sunset_**, the brightness begins to gradually decrease from `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches `min_brightness` at `time=sunset_time + brightness_mode_time_dark`.
+- During **_sunrise_**, the brightness begins to gradually increase from `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches `max_brightness` at `time=sunrise_time + brightness_mode_time_light`.
+
+When `brightness_mode` is set to `"tanh"`, it uses the smooth transition of a [hyperbolic tangent function](https://mathworld.wolfram.com/HyperbolicTangent.html):
+
+- During **_sunset_**, the brightness starts to decrease from 95% of `max_brightness` starting at `time=sunset_time - brightness_mode_time_light`, until it reaches 5% of `min_brightness` at `time=sunset_time + brightness_mode_time_dark`.
+- During **_sunrise_**, the brightness starts to increase from 5% of `min_brightness` starting at `time=sunrise_time - brightness_mode_time_dark`, until it reaches 95% of `max_brightness` at `time=sunrise_time + brightness_mode_time_light`.
+
+
+Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark` in the text box.
+
+
+
+
+
+> [*Code to make the plots*](https://github.com/basnijholt/adaptive-lighting/pull/699#issuecomment-1666232555)
+
## :eyes: See also
- [*Sleep better with Adaptive Lighting in Home Assistant*](https://wartner.io/sleep-better-with-adaptive-lightning-in-home-assistant/) by Florian Wartner on 2023-02-23 (blog post 📜)
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index f204e1ac..a8b53afd 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -141,6 +141,28 @@ DOCS[
CONF_MIN_SUNSET_TIME
] = "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇"
+CONF_BRIGHTNESS_MODE, DEFAULT_BRIGHTNESS_MODE = "brightness_mode", "default"
+DOCS[CONF_BRIGHTNESS_MODE] = (
+ "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` "
+ "(uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈"
+)
+CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK = (
+ "brightness_mode_time_dark",
+ 900,
+)
+DOCS[CONF_BRIGHTNESS_MODE_TIME_DARK] = (
+ "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down "
+ "the brightness before/after sunrise/sunset. 📈📉"
+)
+CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT = (
+ "brightness_mode_time_light",
+ 3600,
+)
+DOCS[CONF_BRIGHTNESS_MODE_TIME_LIGHT] = (
+ "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down "
+ "the brightness after/before sunrise/sunset. 📈📉."
+)
+
CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL = "take_over_control", True
DOCS[CONF_TAKE_OVER_CONTROL] = (
"Disable Adaptive Lighting if another source calls `light.turn_on` while lights "
@@ -282,6 +304,20 @@ VALIDATION_TUPLES = [
(CONF_SUNSET_TIME, NONE_STR, str),
(CONF_MIN_SUNSET_TIME, NONE_STR, str),
(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int),
+ (
+ CONF_BRIGHTNESS_MODE,
+ DEFAULT_BRIGHTNESS_MODE,
+ selector.SelectSelector(
+ selector.SelectSelectorConfig(
+ options=["default", "linear", "tanh"],
+ multiple=False,
+ mode=selector.SelectSelectorMode.DROPDOWN,
+ ),
+ ),
+ ),
+ (CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK, int),
+ (CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT, int),
+ (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool),
(CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool),
(
@@ -321,6 +357,8 @@ EXTRA_VALIDATION = {
CONF_SUNSET_OFFSET: (cv.time_period, timedelta_as_int),
CONF_SUNSET_TIME: (cv.time, str),
CONF_MIN_SUNSET_TIME: (cv.time, str),
+ CONF_BRIGHTNESS_MODE_TIME_LIGHT: (cv.time_period, timedelta_as_int),
+ CONF_BRIGHTNESS_MODE_TIME_DARK: (cv.time_period, timedelta_as_int),
}
diff --git a/custom_components/adaptive_lighting/helpers.py b/custom_components/adaptive_lighting/helpers.py
new file mode 100644
index 00000000..95c87735
--- /dev/null
+++ b/custom_components/adaptive_lighting/helpers.py
@@ -0,0 +1,206 @@
+"""Helper functions for the Adaptive Lighting custom components."""
+
+from __future__ import annotations
+
+import base64
+import colorsys
+import logging
+import math
+from typing import cast
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def clamp(value: float, minimum: float, maximum: float) -> float:
+ """Clamp value between minimum and maximum."""
+ return max(minimum, min(value, maximum))
+
+
+def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]:
+ """Compute the values of 'a' and 'b' for a scaled and shifted tanh function.
+
+ Given two points (x1, y1) and (x2, y2), this function calculates the coefficients 'a' and 'b'
+ for a tanh function of the form y = 0.5 * (tanh(a * (x - b)) + 1) that passes through these points.
+
+ The derivation is as follows:
+
+ 1. Start with the equation of the tanh function:
+ y = 0.5 * (tanh(a * (x - b)) + 1)
+
+ 2. Rearrange the equation to isolate tanh:
+ tanh(a * (x - b)) = 2*y - 1
+
+ 3. Take the inverse tanh (or artanh) on both sides to solve for 'a' and 'b':
+ a * (x - b) = artanh(2*y - 1)
+
+ 4. Plug in the points (x1, y1) and (x2, y2) to get two equations.
+ Using these, we can solve for 'a' and 'b' as:
+ a = (artanh(2*y2 - 1) - artanh(2*y1 - 1)) / (x2 - x1)
+ b = x1 - (artanh(2*y1 - 1) / a)
+
+ Parameters
+ ----------
+ x1
+ x-coordinate of the first point.
+ x2
+ x-coordinate of the second point.
+ y1
+ y-coordinate of the first point (should be between 0 and 1).
+ y2
+ y-coordinate of the second point (should be between 0 and 1).
+
+ Returns
+ -------
+ a
+ Coefficient 'a' for the tanh function.
+ b
+ Coefficient 'b' for the tanh function.
+
+ Notes
+ -----
+ The values of y1 and y2 should lie between 0 and 1, inclusive.
+ """
+ a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1)
+ b = x1 - (math.atanh(2 * y1 - 1) / a)
+ return a, b
+
+
+def scaled_tanh(
+ x: float,
+ a: float,
+ b: float,
+ y_min: float = 0.0,
+ y_max: float = 100.0,
+) -> float:
+ """Apply a scaled and shifted tanh function to a given input.
+
+ This function represents a transformation of the tanh function that scales and shifts
+ the output to lie between y_min and y_max. For values of 'x' close to 'x1' and 'x2'
+ (used to calculate 'a' and 'b'), the output of this function will be close to 'y_min'
+ and 'y_max', respectively.
+
+ The equation of the function is as follows:
+ y = y_min + (y_max - y_min) * 0.5 * (tanh(a * (x - b)) + 1)
+
+ Parameters
+ ----------
+ x
+ The input to the function.
+ a
+ The scale factor for the tanh function, found using 'find_a_b' function.
+ b
+ The shift factor for the tanh function, found using 'find_a_b' function.
+ y_min
+ The minimum value of the output range. Defaults to 0.
+ y_max
+ The maximum value of the output range. Defaults to 100.
+
+ Returns
+ -------
+ float: The output of the function, which lies in the range [y_min, y_max].
+ """
+ return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1)
+
+
+def lerp_color_hsv(
+ rgb1: tuple[float, float, float],
+ rgb2: tuple[float, float, float],
+ t: float,
+) -> tuple[int, int, int]:
+ """Linearly interpolate between two RGB colors in HSV color space."""
+ t = abs(t)
+ assert 0 <= t <= 1
+
+ # Convert RGB to HSV
+ hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1])
+ hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2])
+
+ # Linear interpolation in HSV space
+ hsv = (
+ hsv1[0] + t * (hsv2[0] - hsv1[0]),
+ hsv1[1] + t * (hsv2[1] - hsv1[1]),
+ hsv1[2] + t * (hsv2[2] - hsv1[2]),
+ )
+
+ # Convert back to RGB
+ rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv))
+ assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}"
+ return cast(tuple[int, int, int], rgb)
+
+
+def lerp(x, x1, x2, y1, y2):
+ """Linearly interpolate between two values."""
+ return y1 + (x - x1) * (y2 - y1) / (x2 - x1)
+
+
+def int_to_base36(num: int) -> str:
+ """Convert an integer to its base-36 representation using numbers and uppercase letters.
+
+ Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive
+ alphanumeric representation. The function takes an integer `num` as input and returns
+ its base-36 representation as a string.
+
+ Parameters
+ ----------
+ num
+ The integer to convert to base-36.
+
+ Returns
+ -------
+ str
+ The base-36 representation of the input integer.
+
+ Examples
+ --------
+ >>> num = 123456
+ >>> base36_num = int_to_base36(num)
+ >>> print(base36_num)
+ '2N9'
+ """
+ alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+ if num == 0:
+ return alphanumeric_chars[0]
+
+ base36_str = ""
+ base = len(alphanumeric_chars)
+
+ while num:
+ num, remainder = divmod(num, base)
+ base36_str = alphanumeric_chars[remainder] + base36_str
+
+ return base36_str
+
+
+def short_hash(string: str, length: int = 4) -> str:
+ """Create a hash of 'string' with length 'length'."""
+ return base64.b32encode(string.encode()).decode("utf-8").zfill(length)[:length]
+
+
+def remove_vowels(input_str: str, length: int = 4) -> str:
+ """Remove vowels from a string and return a string of length 'length'."""
+ vowels = "aeiouAEIOU"
+ output_str = "".join([char for char in input_str if char not in vowels])
+ return output_str.zfill(length)[:length]
+
+
+def color_difference_redmean(
+ rgb1: tuple[float, float, float],
+ rgb2: tuple[float, float, float],
+) -> float:
+ """Distance between colors in RGB space (redmean metric).
+
+ The maximal distance between (255, 255, 255) and (0, 0, 0) ≈ 765.
+
+ Sources:
+ - https://en.wikipedia.org/wiki/Color_difference#Euclidean
+ - https://www.compuphase.com/cmetric.htm
+ """
+ r_hat = (rgb1[0] + rgb2[0]) / 2
+ delta_r, delta_g, delta_b = (
+ (col1 - col2) for col1, col2 in zip(rgb1, rgb2, strict=True)
+ )
+ red_term = (2 + r_hat / 256) * delta_r**2
+ green_term = 4 * delta_g**2
+ blue_term = (2 + (255 - r_hat) / 256) * delta_b**2
+ return math.sqrt(red_term + green_term + blue_term)
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index a942e66b..0751500b 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -40,10 +40,13 @@
"sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
"min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
+ "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
+ "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
+ "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
+ "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
- "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index defe3b85..a32406d5 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -2,9 +2,7 @@
from __future__ import annotations
import asyncio
-import base64
import bisect
-import colorsys
import datetime
import functools
import logging
@@ -12,7 +10,7 @@ import math
from copy import deepcopy
from dataclasses import dataclass
from datetime import timedelta
-from typing import TYPE_CHECKING, Any, Literal, cast
+from typing import TYPE_CHECKING, Any, Literal
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
@@ -108,6 +106,9 @@ from .const import (
CONF_ADAPT_DELAY,
CONF_ADAPT_UNTIL_SLEEP,
CONF_AUTORESET_CONTROL,
+ CONF_BRIGHTNESS_MODE,
+ CONF_BRIGHTNESS_MODE_TIME_DARK,
+ CONF_BRIGHTNESS_MODE_TIME_LIGHT,
CONF_DETECT_NON_HA_CHANGES,
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES,
CONF_INITIAL_TRANSITION,
@@ -158,6 +159,17 @@ from .const import (
replace_none_str,
)
from .hass_utils import setup_service_call_interceptor
+from .helpers import (
+ clamp,
+ color_difference_redmean,
+ find_a_b,
+ int_to_base36,
+ lerp,
+ lerp_color_hsv,
+ remove_vowels,
+ scaled_tanh,
+ short_hash,
+)
if TYPE_CHECKING:
from collections.abc import Callable, Coroutine, Iterable
@@ -195,56 +207,6 @@ RGB_REDMEAN_CHANGE = 80 # ≈10% of total range
_DOMAIN_SHORT = "al"
-def _int_to_base36(num: int) -> str:
- """Convert an integer to its base-36 representation using numbers and uppercase letters.
-
- Base-36 encoding uses digits 0-9 and uppercase letters A-Z, providing a case-insensitive
- alphanumeric representation. The function takes an integer `num` as input and returns
- its base-36 representation as a string.
-
- Parameters
- ----------
- num
- The integer to convert to base-36.
-
- Returns
- -------
- str
- The base-36 representation of the input integer.
-
- Examples
- --------
- >>> num = 123456
- >>> base36_num = int_to_base36(num)
- >>> print(base36_num)
- '2N9'
- """
- alphanumeric_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-
- if num == 0:
- return alphanumeric_chars[0]
-
- base36_str = ""
- base = len(alphanumeric_chars)
-
- while num:
- num, remainder = divmod(num, base)
- base36_str = alphanumeric_chars[remainder] + base36_str
-
- return base36_str
-
-
-def _short_hash(string: str, length: int = 4) -> str:
- """Create a hash of 'string' with length 'length'."""
- return base64.b32encode(string.encode()).decode("utf-8").zfill(length)[:length]
-
-
-def _remove_vowels(input_str: str, length: int = 4) -> str:
- vowels = "aeiouAEIOU"
- output_str = "".join([char for char in input_str if char not in vowels])
- return output_str.zfill(length)[:length]
-
-
def create_context(
name: str,
which: str,
@@ -257,11 +219,11 @@ def create_context(
# Pack index with base85 to maximize the number of contexts we can create
# before we exceed the 26-character limit and are forced to wrap.
time_stamp = ulid_transform.ulid_now()[:10] # time part of a ULID
- name_hash = _short_hash(name)
- which_short = _remove_vowels(which)
+ name_hash = short_hash(name)
+ which_short = remove_vowels(which)
context_id_start = f"{time_stamp}:{_DOMAIN_SHORT}:{name_hash}:{which_short}:"
chars_left = 26 - len(context_id_start)
- index_packed = _int_to_base36(index).zfill(chars_left)[-chars_left:]
+ index_packed = int_to_base36(index).zfill(chars_left)[-chars_left:]
context_id = context_id_start + index_packed
parent_id = parent.id if parent else None
return Context(id=context_id, parent_id=parent_id)
@@ -277,7 +239,7 @@ def is_our_context_id(context_id: str | None, which: str | None = None) -> bool:
return False
if which is None:
return True
- return f":{_remove_vowels(which)}:" in context_id
+ return f":{remove_vowels(which)}:" in context_id
def is_our_context(context: Context | None, which: str | None = None) -> bool:
@@ -716,28 +678,6 @@ def _supported_features(hass: HomeAssistant, light: str) -> set[str]:
return supported
-def color_difference_redmean(
- rgb1: tuple[float, float, float],
- rgb2: tuple[float, float, float],
-) -> float:
- """Distance between colors in RGB space (redmean metric).
-
- The maximal distance between (255, 255, 255) and (0, 0, 0) ≈ 765.
-
- Sources:
- - https://en.wikipedia.org/wiki/Color_difference#Euclidean
- - https://www.compuphase.com/cmetric.htm
- """
- r_hat = (rgb1[0] + rgb2[0]) / 2
- delta_r, delta_g, delta_b = (
- (col1 - col2) for col1, col2 in zip(rgb1, rgb2, strict=True)
- )
- red_term = (2 + r_hat / 256) * delta_r**2
- green_term = 4 * delta_g**2
- blue_term = (2 + (255 - r_hat) / 256) * delta_b**2
- return math.sqrt(red_term + green_term + blue_term)
-
-
# All comparisons should be done with RGB since
# converting anything to color temp is inaccurate.
def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]:
@@ -968,6 +908,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
sunset_offset=data[CONF_SUNSET_OFFSET],
sunset_time=data[CONF_SUNSET_TIME],
min_sunset_time=data[CONF_MIN_SUNSET_TIME],
+ brightness_mode=data[CONF_BRIGHTNESS_MODE],
+ brightness_mode_time_dark=data[CONF_BRIGHTNESS_MODE_TIME_DARK],
+ brightness_mode_time_light=data[CONF_BRIGHTNESS_MODE_TIME_LIGHT],
transition=data[CONF_TRANSITION],
)
_LOGGER.debug(
@@ -1251,7 +1194,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
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 = max(min(color_temp_kelvin, max_kelvin), min_kelvin)
+ color_temp_kelvin = clamp(color_temp_kelvin, min_kelvin, max_kelvin)
service_data[ATTR_COLOR_TEMP_KELVIN] = color_temp_kelvin
elif "color" in features and adapt_color:
_LOGGER.debug("%s: Setting rgb_color of light %s", self._name, light)
@@ -1614,32 +1557,6 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
self._state = False
-def lerp_color_hsv(
- rgb1: tuple[float, float, float],
- rgb2: tuple[float, float, float],
- t: float,
-) -> tuple[int, int, int]:
- """Linearly interpolate between two RGB colors in HSV color space."""
- t = abs(t)
- assert 0 <= t <= 1
-
- # Convert RGB to HSV
- hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1])
- hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2])
-
- # Linear interpolation in HSV space
- hsv = (
- hsv1[0] + t * (hsv2[0] - hsv1[0]),
- hsv1[1] + t * (hsv2[1] - hsv1[1]),
- hsv1[2] + t * (hsv2[2] - hsv1[2]),
- )
-
- # Convert back to RGB
- rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv))
- assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}"
- return cast(tuple[int, int, int], rgb)
-
-
@dataclass(frozen=True)
class SunLightSettings:
"""Track the state of the sun and associated light settings."""
@@ -1661,18 +1578,47 @@ class SunLightSettings:
sunset_offset: datetime.timedelta | None
sunset_time: datetime.time | None
min_sunset_time: datetime.time | None
+ brightness_mode: Literal["default", "linear", "tanh"]
+ brightness_mode_time_dark: datetime.timedelta | None
+ brightness_mode_time_light: datetime.timedelta | None
transition: int
+ def sunrise(self, date: datetime.datetime) -> datetime.datetime:
+ """Return the (adjusted) sunrise time for the given date."""
+ sunrise = (
+ self.astral_location.sunrise(date, local=False)
+ if self.sunrise_time is None
+ else self._replace_time(date, "sunrise")
+ ) + self.sunrise_offset
+ if self.max_sunrise_time is not None:
+ max_sunrise = self._replace_time(date, "max_sunrise")
+ if max_sunrise < sunrise:
+ sunrise = max_sunrise
+ return sunrise
+
+ def sunset(self, date: datetime.datetime) -> datetime.datetime:
+ """Return the (adjusted) sunset time for the given date."""
+ sunset = (
+ self.astral_location.sunset(date, local=False)
+ if self.sunset_time is None
+ else self._replace_time(date, "sunset")
+ ) + self.sunset_offset
+ if self.min_sunset_time is not None:
+ min_sunset = self._replace_time(date, "min_sunset")
+ if min_sunset > sunset:
+ sunset = min_sunset
+ return sunset
+
+ def _replace_time(self, date: datetime.datetime, key: str) -> datetime.datetime:
+ time = getattr(self, f"{key}_time")
+ date_time = datetime.datetime.combine(date, time)
+ return date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(
+ dt_util.UTC,
+ )
+
def get_sun_events(self, date: datetime.datetime) -> list[tuple[str, float]]:
"""Get the four sun event's timestamps at 'date'."""
- def _replace_time(date: datetime.datetime, key: str) -> datetime.datetime:
- time = getattr(self, f"{key}_time")
- date_time = datetime.datetime.combine(date, time)
- return date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(
- dt_util.UTC,
- )
-
def calculate_noon_and_midnight(
sunset: datetime.datetime,
sunrise: datetime.datetime,
@@ -1689,27 +1635,8 @@ class SunLightSettings:
return noon, midnight
location = self.astral_location
-
- sunrise = (
- location.sunrise(date, local=False)
- if self.sunrise_time is None
- else _replace_time(date, "sunrise")
- ) + self.sunrise_offset
- sunset = (
- location.sunset(date, local=False)
- if self.sunset_time is None
- else _replace_time(date, "sunset")
- ) + self.sunset_offset
-
- if self.max_sunrise_time is not None:
- max_sunrise = _replace_time(date, "max_sunrise")
- if max_sunrise < sunrise:
- sunrise = max_sunrise
-
- if self.min_sunset_time is not None:
- min_sunset = _replace_time(date, "min_sunset")
- if min_sunset > sunset:
- sunset = min_sunset
+ sunrise = self.sunrise(date)
+ sunset = self.sunset(date)
if (
self.sunrise_time is None
@@ -1774,11 +1701,75 @@ class SunLightSettings:
"""Calculate the brightness in %."""
if is_sleep:
return self.sleep_brightness
- if percent > 0:
- return self.max_brightness
- delta_brightness = self.max_brightness - self.min_brightness
- percent = 1 + percent
- return (delta_brightness * percent) + self.min_brightness
+ assert self.brightness_mode in ("default", "linear", "tanh")
+
+ if self.brightness_mode == "default":
+ if percent > 0:
+ return self.max_brightness
+ delta_brightness = self.max_brightness - self.min_brightness
+ percent = 1 + percent
+ return (delta_brightness * percent) + self.min_brightness
+
+ now = dt_util.utcnow()
+ (prev_event, prev_ts), (next_event, next_ts) = self.relevant_events(now)
+
+ # at ts_event - dt_start, brightness == start_brightness
+ # at ts_event + dt_end, brightness == end_brightness
+ dark = (self.brightness_mode_time_dark or timedelta()).total_seconds()
+ light = (self.brightness_mode_time_light or timedelta()).total_seconds()
+ # Handle sunrise
+ if prev_event == SUN_EVENT_SUNRISE or next_event == SUN_EVENT_SUNRISE:
+ ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts
+ if self.brightness_mode == "linear":
+ brightness = lerp(
+ now.timestamp(),
+ x1=ts_event - dark,
+ x2=ts_event + light,
+ y1=self.min_brightness,
+ y2=self.max_brightness,
+ )
+ else:
+ assert self.brightness_mode == "tanh"
+ a, b = find_a_b(
+ x1=-dark,
+ x2=+light,
+ y1=0.05, # be at 5% of range at x1
+ y2=0.95, # be at 95% of range at x2
+ )
+ brightness = scaled_tanh(
+ now.timestamp() - ts_event,
+ a=a,
+ b=b,
+ y_min=self.min_brightness,
+ y_max=self.max_brightness,
+ )
+ # Handle sunset
+ elif prev_event == SUN_EVENT_SUNSET or next_event == SUN_EVENT_SUNSET:
+ ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts
+ if self.brightness_mode == "linear":
+ brightness = lerp(
+ now.timestamp(),
+ x1=ts_event - light,
+ x2=ts_event + dark,
+ y1=self.max_brightness,
+ y2=self.min_brightness,
+ )
+ else:
+ assert self.brightness_mode == "tanh"
+ a, b = find_a_b(
+ x1=-light, # shifted timestamp for the start of sunset
+ x2=+dark, # shifted timestamp for the end of sunset
+ y1=0.95, # be at 95% of range at the start of sunset
+ y2=0.05, # be at 5% of range at the end of sunset
+ )
+ brightness = scaled_tanh(
+ now.timestamp() - ts_event,
+ a=a,
+ b=b,
+ y_min=self.min_brightness,
+ y_max=self.max_brightness,
+ )
+ return clamp(brightness, self.min_brightness, self.max_brightness)
def calc_color_temp_kelvin(self, percent: float) -> int:
"""Calculate the color temperature in Kelvin."""
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index b27ee3c4..511a11c8 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -41,10 +41,13 @@
"sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
"min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
+ "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
+ "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
+ "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
+ "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
- "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
diff --git a/tests/test_switch.py b/tests/test_switch.py
index e3bc9126..1a25c7a1 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -60,6 +60,9 @@ from custom_components.adaptive_lighting.const import (
ATTR_ADAPTIVE_LIGHTING_MANAGER,
CONF_ADAPT_UNTIL_SLEEP,
CONF_AUTORESET_CONTROL,
+ CONF_BRIGHTNESS_MODE,
+ CONF_BRIGHTNESS_MODE_TIME_DARK,
+ CONF_BRIGHTNESS_MODE_TIME_LIGHT,
CONF_DETECT_NON_HA_CHANGES,
CONF_INITIAL_TRANSITION,
CONF_MANUAL_CONTROL,
@@ -364,7 +367,11 @@ async def test_adaptive_lighting_time_zones_with_default_settings(
@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS)
async def test_adaptive_lighting_time_zones_and_sun_settings(
- hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name
+ hass,
+ lat,
+ long,
+ timezone,
+ reset_time_zone, # pylint: disable=redefined-outer-name
):
"""Test setting up the Adaptive Lighting switches with different timezones.
@@ -1992,3 +1999,83 @@ async def test_light_group(
]
assert ":skpp:" in events[2].context.id
assert len(events) == 3
+
+
+@pytest.mark.parametrize("brightness_mode", ["linear", "tanh"])
+@pytest.mark.parametrize("dark,light", ([900, 1800], [1800, 900], [1800, 1800]))
+async def test_brightness_mode(hass, brightness_mode, dark, light):
+ """Test brightness mode.
+
+ We are not testing the "default" mode because that is tested in all other tests.
+ """
+ is_symmetric = dark == light
+ _, switch = await setup_switch(
+ hass,
+ {
+ CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour),
+ CONF_SUNSET_TIME: datetime.time(SUNSET.hour),
+ CONF_BRIGHTNESS_MODE: brightness_mode,
+ CONF_BRIGHTNESS_MODE_TIME_DARK: datetime.timedelta(seconds=dark),
+ CONF_BRIGHTNESS_MODE_TIME_LIGHT: datetime.timedelta(seconds=light),
+ },
+ )
+
+ context = switch.create_context("test") # needs to be passed to update method
+ min_brightness = switch._sun_light_settings.min_brightness
+ max_brightness = switch._sun_light_settings.max_brightness
+ brightness_range = max_brightness - min_brightness
+ brightness_event = min_brightness + brightness_range / 2
+ dark = switch._sun_light_settings.brightness_mode_time_dark
+ light = switch._sun_light_settings.brightness_mode_time_light
+
+ sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC)
+ before_sunset = sunset - light
+ after_sunset = sunset + dark
+ sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC)
+ before_sunrise = sunrise - dark
+ after_sunrise = sunrise + light
+
+ light_brightness = (
+ max_brightness
+ if brightness_mode == "linear"
+ else 0.95 * brightness_range + min_brightness
+ )
+ dark_brightness = (
+ min_brightness
+ if brightness_mode == "linear"
+ else 0.05 * brightness_range + min_brightness
+ )
+
+ def is_approx_equal(a, b):
+ return abs(a - b) < 0.01
+
+ async def patch_time_and_update(time):
+ with patch("homeassistant.util.dt.utcnow", return_value=time):
+ await switch._update_attrs_and_maybe_adapt_lights(context=context)
+ await hass.async_block_till_done()
+
+ if is_symmetric:
+ # At sunset the brightness should be 50%
+ await patch_time_and_update(sunset)
+ assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], brightness_event)
+
+ # Before sunset the brightness should be max
+ await patch_time_and_update(before_sunset)
+ assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness)
+
+ # After sunset the brightness should be dark_brightness
+ await patch_time_and_update(after_sunset)
+ assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], dark_brightness)
+
+ if is_symmetric:
+ # At sunrise the brightness should be 50%
+ await patch_time_and_update(sunrise)
+ assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], brightness_event)
+
+ # Before sunrise the brightness should be min
+ await patch_time_and_update(before_sunrise)
+ assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], dark_brightness)
+
+ # After sunrise the brightness should be light_brightness
+ await patch_time_and_update(after_sunrise)
+ assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness)
From e3a84ffd9668386806ac4582f89bb182154d4de1 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 5 Aug 2023 13:52:54 -0700
Subject: [PATCH 095/549] Logging fixes in significant_changes (#704)
---
custom_components/adaptive_lighting/switch.py | 32 ++++++++-----------
1 file changed, 14 insertions(+), 18 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index a32406d5..85619460 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio
import bisect
import datetime
-import functools
import logging
import math
from copy import deepcopy
@@ -2330,6 +2329,12 @@ class AdaptiveLightingManager:
delay = self.auto_reset_manual_control_times.get(light)
async def reset():
+ _LOGGER.debug(
+ "Auto resetting 'manual_control' status of '%s' because"
+ " it was not manually controlled for %s seconds.",
+ light,
+ delay,
+ )
self.reset(light)
switches = _switches_with_lights(self.hass, [light])
for switch in switches:
@@ -2341,12 +2346,6 @@ class AdaptiveLightingManager:
transition=switch.initial_transition,
force=True,
)
- _LOGGER.debug(
- "Auto resetting 'manual_control' status of '%s' because"
- " it was not manually controlled for %s seconds.",
- light,
- delay,
- )
assert not self.manual_control[light]
self._handle_timer(light, self.auto_reset_manual_control_timers, delay, reset)
@@ -2384,7 +2383,7 @@ class AdaptiveLightingManager:
# color_task might be the same as brightness_task
color_task.cancel()
- def reset(self, *lights, reset_manual_control=True) -> None:
+ def reset(self, *lights, reset_manual_control: bool = True) -> None:
"""Reset the 'manual_control' status of the lights."""
for light in lights:
if reset_manual_control:
@@ -2653,13 +2652,6 @@ class AdaptiveLightingManager:
last_service_data = self.last_service_data.get(light)
if last_service_data is None:
return False
- compare_to = functools.partial(
- _attributes_have_changed,
- light=light,
- adapt_brightness=adapt_brightness,
- adapt_color=adapt_color,
- context=context,
- )
# Update state and check for a manual change not done in HA.
# Ensure HASS is correctly updating your light's state with
# light.turn_on calls if any problems arise. This
@@ -2668,13 +2660,17 @@ class AdaptiveLightingManager:
refreshed_state = self.hass.states.get(light)
assert refreshed_state is not None
- changed = compare_to(
+ changed = _attributes_have_changed(
old_attributes=last_service_data,
new_attributes=refreshed_state.attributes,
+ light=light,
+ adapt_brightness=adapt_brightness,
+ adapt_color=adapt_color,
+ context=context,
)
if changed:
_LOGGER.debug(
- "%s: State attributes of '%s' (%s) didn't change wrt 'last_service_data' (%s) (context.id=%s)",
+ "%s: State attributes of '%s' changed (%s) wrt 'last_service_data' (%s) (context.id=%s)",
switch._name,
light,
refreshed_state.attributes,
@@ -2683,7 +2679,7 @@ class AdaptiveLightingManager:
)
return True
_LOGGER.debug(
- "%s: State attributes of '%s' (%s) changed wrt 'last_service_data' (%s) (context.id=%s)",
+ "%s: State attributes of '%s' did not change (%s) wrt 'last_service_data' (%s) (context.id=%s)",
switch._name,
light,
refreshed_state.attributes,
From 51a18f2afb445f9938cded66b952bf1035d37faa Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 5 Aug 2023 13:59:58 -0700
Subject: [PATCH 096/549] Allow adaptive_lighting.apply to turn on lights and
do not issue log message (#705)
---
custom_components/adaptive_lighting/switch.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 85619460..afeb7fa7 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -2694,7 +2694,10 @@ class AdaptiveLightingManager:
off_to_on_event: Event,
) -> bool:
# Adaptive Lighting should never turn on lights itself
- if is_our_context(off_to_on_event.context):
+ if is_our_context(off_to_on_event.context) and not is_our_context(
+ off_to_on_event.context,
+ "service", # adaptive_lighting.apply is allowed to turn on lights
+ ):
_LOGGER.warning(
"Detected an 'off' → 'on' event for '%s' with context.id='%s' and"
" event='%s', triggered by the adaptive_lighting integration itself,"
From 11a4489cb5b996e924966eba145833161641aac7 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 5 Aug 2023 14:42:40 -0700
Subject: [PATCH 097/549] Bump to 1.19.0b1 manifest.json (#706)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 7806e04a..6202a765 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.18.3"
+ "version": "1.19.0b1"
}
From 72419cb60f983a38a8b3ad16973b401b6cbc62b2 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 5 Aug 2023 15:08:45 -0700
Subject: [PATCH 098/549] Add a min_sunrise_time and max_sunset_time (#707)
---
README.md | 2 ++
custom_components/adaptive_lighting/const.py | 12 ++++++++++++
.../adaptive_lighting/strings.json | 2 ++
custom_components/adaptive_lighting/switch.py | 18 ++++++++++++++++--
.../adaptive_lighting/translations/en.json | 2 ++
5 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 71500862..b83d880f 100644
--- a/README.md
+++ b/README.md
@@ -115,10 +115,12 @@ The YAML and frontend configuration methods support all of the options listed be
| `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` |
| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
+| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` |
| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` |
| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` |
+| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` |
| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` |
| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` |
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index a8b53afd..1d060c56 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -122,6 +122,11 @@ DOCS[
CONF_SUNRISE_TIME = "sunrise_time"
DOCS[CONF_SUNRISE_TIME] = "Set a fixed time (HH:MM:SS) for sunrise. 🌅"
+CONF_MIN_SUNRISE_TIME = "min_sunrise_time"
+DOCS[
+ CONF_MIN_SUNRISE_TIME
+] = "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅"
+
CONF_MAX_SUNRISE_TIME = "max_sunrise_time"
DOCS[CONF_MAX_SUNRISE_TIME] = (
"Set the latest virtual sunrise time (HH:MM:SS), allowing"
@@ -141,6 +146,11 @@ DOCS[
CONF_MIN_SUNSET_TIME
] = "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇"
+CONF_MAX_SUNSET_TIME = "max_sunset_time"
+DOCS[
+ CONF_MAX_SUNSET_TIME
+] = "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇"
+
CONF_BRIGHTNESS_MODE, DEFAULT_BRIGHTNESS_MODE = "brightness_mode", "default"
DOCS[CONF_BRIGHTNESS_MODE] = (
"Brightness mode to use. Possible values are `default`, `linear`, and `tanh` "
@@ -299,10 +309,12 @@ VALIDATION_TUPLES = [
(CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION),
(CONF_ADAPT_UNTIL_SLEEP, DEFAULT_ADAPT_UNTIL_SLEEP, bool),
(CONF_SUNRISE_TIME, NONE_STR, str),
+ (CONF_MIN_SUNRISE_TIME, NONE_STR, str),
(CONF_MAX_SUNRISE_TIME, NONE_STR, str),
(CONF_SUNRISE_OFFSET, DEFAULT_SUNRISE_OFFSET, int),
(CONF_SUNSET_TIME, NONE_STR, str),
(CONF_MIN_SUNSET_TIME, NONE_STR, str),
+ (CONF_MAX_SUNSET_TIME, NONE_STR, str),
(CONF_SUNSET_OFFSET, DEFAULT_SUNSET_OFFSET, int),
(
CONF_BRIGHTNESS_MODE,
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index 0751500b..cece3da7 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -35,10 +35,12 @@
"sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled 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. 🌙",
"sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
+ "min_sunrise_time": "min_sunrise_time: Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
"max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
"sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
"sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
"min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
+ "max_sunset_time": "max_sunset_time: Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇",
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
"brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
"brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index afeb7fa7..b5bf65de 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -117,8 +117,10 @@ from .const import (
CONF_MAX_BRIGHTNESS,
CONF_MAX_COLOR_TEMP,
CONF_MAX_SUNRISE_TIME,
+ CONF_MAX_SUNSET_TIME,
CONF_MIN_BRIGHTNESS,
CONF_MIN_COLOR_TEMP,
+ CONF_MIN_SUNRISE_TIME,
CONF_MIN_SUNSET_TIME,
CONF_MULTI_LIGHT_INTERCEPT,
CONF_ONLY_ONCE,
@@ -903,10 +905,12 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
sleep_rgb_or_color_temp=data[CONF_SLEEP_RGB_OR_COLOR_TEMP],
sunrise_offset=data[CONF_SUNRISE_OFFSET],
sunrise_time=data[CONF_SUNRISE_TIME],
+ min_sunrise_time=data[CONF_MIN_SUNRISE_TIME],
max_sunrise_time=data[CONF_MAX_SUNRISE_TIME],
sunset_offset=data[CONF_SUNSET_OFFSET],
sunset_time=data[CONF_SUNSET_TIME],
min_sunset_time=data[CONF_MIN_SUNSET_TIME],
+ max_sunset_time=data[CONF_MAX_SUNSET_TIME],
brightness_mode=data[CONF_BRIGHTNESS_MODE],
brightness_mode_time_dark=data[CONF_BRIGHTNESS_MODE_TIME_DARK],
brightness_mode_time_light=data[CONF_BRIGHTNESS_MODE_TIME_LIGHT],
@@ -1571,12 +1575,14 @@ class SunLightSettings:
sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"]
sleep_color_temp: int
sleep_rgb_color: tuple[int, int, int]
- sunrise_offset: datetime.timedelta | None
sunrise_time: datetime.time | None
+ sunrise_offset: datetime.timedelta | None
+ min_sunrise_time: datetime.time | None
max_sunrise_time: datetime.time | None
- sunset_offset: datetime.timedelta | None
sunset_time: datetime.time | None
+ sunset_offset: datetime.timedelta | None
min_sunset_time: datetime.time | None
+ max_sunset_time: datetime.time | None
brightness_mode: Literal["default", "linear", "tanh"]
brightness_mode_time_dark: datetime.timedelta | None
brightness_mode_time_light: datetime.timedelta | None
@@ -1589,6 +1595,10 @@ class SunLightSettings:
if self.sunrise_time is None
else self._replace_time(date, "sunrise")
) + self.sunrise_offset
+ if self.min_sunrise_time is not None:
+ min_sunrise = self._replace_time(date, "min_sunrise")
+ if min_sunrise > sunrise:
+ sunrise = min_sunrise
if self.max_sunrise_time is not None:
max_sunrise = self._replace_time(date, "max_sunrise")
if max_sunrise < sunrise:
@@ -1606,6 +1616,10 @@ class SunLightSettings:
min_sunset = self._replace_time(date, "min_sunset")
if min_sunset > sunset:
sunset = min_sunset
+ if self.max_sunset_time is not None:
+ max_sunset = self._replace_time(date, "max_sunset")
+ if max_sunset < sunset:
+ sunset = max_sunset
return sunset
def _replace_time(self, date: datetime.datetime, key: str) -> datetime.datetime:
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index 511a11c8..c8e2c413 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -36,10 +36,12 @@
"sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled 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. 🌙",
"sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
+ "min_sunrise_time": "min_sunrise_time: Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
"max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
"sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
"sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
"min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
+ "max_sunset_time": "max_sunset_time: Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇",
"sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
"brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
"brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
From 3f17738bd16fa49a92ea87f9690311965e967d95 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 5 Aug 2023 16:48:35 -0700
Subject: [PATCH 099/549] Add warning about Zigbee groups (#708)
---
README.md | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index b83d880f..be753a9f 100644
--- a/README.md
+++ b/README.md
@@ -366,11 +366,21 @@ Ensure your light bulbs have a strong WiFi connection. If the signal strength is
#### :spider_web: Zigbee, Z-Wave, and Other Mesh Networks
-Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant). Philips lights usually function as routers, while Ikea, Sengled, and generic Tuya bulbs often do not. If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue. Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health. Smart plugs can be an affordable way to add more routers to your network.
+Mesh networks typically require powered devices to act as routers, relaying messages back to the central coordinator (the radio connected to Home Assistant).
+Philips lights usually function as routers, while Ikea, Sengled, and generic Tuya bulbs often do not.
+If devices become unresponsive or fail to respond to commands, Adaptive Lighting can exacerbate the issue.
+Use network maps (available in ZHA, zigbee2mqtt, deCONZ, and ZWaveJS UI) to evaluate your network health.
+Smart plugs can be an affordable way to add more routers to your network.
-For most Zigbee networks, **using groups is essential for optimal performance**. For example, if you want to use Adaptive Lighting in a hallway with six bulbs, adding each bulb individually to the Adaptive Lighting configuration could overwhelm the network with commands. Instead, create a group in your Zigbee software (not a regular Home Assistant group) and add that single group to the Adaptive Lighting configuration. This sends a single broadcast command to adjust all bulbs, improving response times and keeping the bulbs in sync.
+For most Zigbee networks, **using groups is essential for optimal performance**.
+For example, if you want to use Adaptive Lighting in a hallway with six bulbs, adding each bulb individually to the Adaptive Lighting configuration could overwhelm the network with commands.
+Instead, create a group in your Zigbee software (not a regular Home Assistant group) and add that single group to the Adaptive Lighting configuration.
+This sends a single broadcast command to adjust all bulbs, improving response times and keeping the bulbs in sync.
-As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group. Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit.
+As a rule of thumb, if you always control lights together (e.g., bulbs in a ceiling fixture), they should be in a Zigbee group.
+Expose only the group (not individual bulbs) in Home Assistant Dashboards and external systems like Google Home or Apple HomeKit.
+
+> :warning: **If you control lights individually, `manual_control` cannot behave correctly! If you need to control lights individually as well, use a [Home Assistant Light Group](https://www.home-assistant.io/integrations/group/).**
#### :rainbow: Light Colors Not Matching
From d44cb66148af86a328e085db58d5d2b3fa03b6a3 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 5 Aug 2023 20:56:32 -0700
Subject: [PATCH 100/549] Add `adapt_only_on_bare_turn_on` which instantly
triggers `manual_control` when turning on with brightness or color (#709)
---
README.md | 78 +++++++++----------
custom_components/adaptive_lighting/const.py | 14 +++-
.../adaptive_lighting/strings.json | 3 +-
custom_components/adaptive_lighting/switch.py | 64 +++++++++++++--
.../adaptive_lighting/translations/en.json | 3 +-
tests/test_switch.py | 22 +++++-
6 files changed, 131 insertions(+), 53 deletions(-)
diff --git a/README.md b/README.md
index be753a9f..56d9f05d 100644
--- a/README.md
+++ b/README.md
@@ -97,45 +97,45 @@ The YAML and frontend configuration methods support all of the options listed be
-| Variable name | Description | Default | Type |
-|:-------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------|
-| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s |
-| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
-| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
-| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
-| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
-| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
-| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
-| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
-| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
-| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
-| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
-| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
-| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
-| `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` |
-| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
-| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` |
-| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` |
-| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
-| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` |
-| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` |
-| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
-| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` |
-| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` |
-| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `int` |
-| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
-| `take_over_control` | Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒 | `True` | `bool` |
-| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` |
-| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
-| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
-| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
-| `send_split_delay` | 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` |
-| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
-| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` |
-| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
+| Variable name | Description | Default | Type |
+|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------------------|
+| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s |
+| `interval` | Frequency to adapt the lights, in seconds. 🔄 | `90` | `int > 0` |
+| `transition` | Duration of transition when lights change, in seconds. 🕑 | `45` | `float` 0-6553 |
+| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 |
+| `min_brightness` | Minimum brightness percentage. 💡 | `1` | `int` 1-100 |
+| `max_brightness` | Maximum brightness percentage. 💡 | `100` | `int` 1-100 |
+| `min_color_temp` | Warmest color temperature in Kelvin. 🔥 | `2000` | `int` 1000-10000 |
+| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 |
+| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` |
+| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 |
+| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. 🌙 | `color_temp` | one of `['color_temp', 'rgb_color']` |
+| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 |
+| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color |
+| `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` |
+| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. 🌅 | `None` | `str` |
+| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅 | `None` | `str` |
+| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅 | `None` | `str` |
+| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. 🌇 | `None` | `str` |
+| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇 | `None` | `str` |
+| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇 | `None` | `str` |
+| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` |
+| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈 | `default` | one of `['default', 'linear', 'tanh']` |
+| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉 | `900` | `int` |
+| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉. | `3600` | `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` |
+| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues. | `False` | `bool` |
+| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 |
+| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄 | `False` | `bool` |
+| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` |
+| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀 | `False` | `bool` |
+| `send_split_delay` | 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` |
+| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
+| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` |
+| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index 1d060c56..f3dc628d 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -77,6 +77,18 @@ DOCS[CONF_ONLY_ONCE] = (
"(`false`). 🔄"
)
+CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON = (
+ "adapt_only_on_bare_turn_on",
+ False,
+)
+DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = (
+ "When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is "
+ "invoked without specifying color or brightness. ❌🌈 "
+ "This e.g., prevents adaptation when activating a scene. "
+ "If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. "
+ "Needs `take_over_control` enabled. 🕵️ "
+)
+
CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False
DOCS[CONF_PREFER_RGB_COLOR] = (
"Whether to prefer RGB color adjustment over "
@@ -329,7 +341,6 @@ VALIDATION_TUPLES = [
),
(CONF_BRIGHTNESS_MODE_TIME_DARK, DEFAULT_BRIGHTNESS_MODE_TIME_DARK, int),
(CONF_BRIGHTNESS_MODE_TIME_LIGHT, DEFAULT_BRIGHTNESS_MODE_TIME_LIGHT, int),
- (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
(CONF_TAKE_OVER_CONTROL, DEFAULT_TAKE_OVER_CONTROL, bool),
(CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES, bool),
(
@@ -338,6 +349,7 @@ VALIDATION_TUPLES = [
int_between(0, 365 * 24 * 60 * 60), # 1 year max
),
(CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool),
+ (CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON, 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_ADAPT_DELAY, DEFAULT_ADAPT_DELAY, cv.positive_float),
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index cece3da7..5e60ed79 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -45,10 +45,11 @@
"brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
"brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
"brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
- "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index b5bf65de..49d886fa 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -103,6 +103,7 @@ from .const import (
ATTR_ADAPT_COLOR,
ATTR_ADAPTIVE_LIGHTING_MANAGER,
CONF_ADAPT_DELAY,
+ CONF_ADAPT_ONLY_ON_BARE_TURN_ON,
CONF_ADAPT_UNTIL_SLEEP,
CONF_AUTORESET_CONTROL,
CONF_BRIGHTNESS_MODE,
@@ -876,15 +877,18 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._adapt_delay = data[CONF_ADAPT_DELAY]
self._send_split_delay = data[CONF_SEND_SPLIT_DELAY]
self._take_over_control = data[CONF_TAKE_OVER_CONTROL]
- self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
- if not data[CONF_TAKE_OVER_CONTROL] and data[CONF_DETECT_NON_HA_CHANGES]:
+ if not data[CONF_TAKE_OVER_CONTROL] and (
+ data[CONF_DETECT_NON_HA_CHANGES] or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON]
+ ):
_LOGGER.warning(
- "%s: Config mismatch: 'detect_non_ha_changes: true' "
- "requires 'take_over_control' to be enabled. Adjusting config "
+ "%s: Config mismatch: `detect_non_ha_changes` or `adapt_only_on_bare_turn_on` "
+ "set to `true` requires `take_over_control` to be enabled. Adjusting config "
"and continuing setup with `take_over_control: true`.",
self._name,
)
self._take_over_control = True
+ self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
+ self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON]
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS]
self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT]
@@ -1446,13 +1450,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None:
assert not self.manager.is_proactively_adapting(event.context.id)
+ from_turn_on = self.manager._off_to_on_state_event_is_from_turn_on(
+ entity_id,
+ event,
+ )
if (
self._take_over_control
and not self._detect_non_ha_changes
- and not self.manager._off_to_on_state_event_is_from_turn_on(
- entity_id,
- event,
- )
+ and not from_turn_on
):
# There is an edge case where 2 switches control the same light, e.g.,
# one for brightness and one for color. Now we will mark both switches
@@ -1468,6 +1473,25 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self.manager.mark_as_manual_control(entity_id)
return
+ if (
+ self._take_over_control
+ and self._adapt_only_on_bare_turn_on
+ and from_turn_on
+ ):
+ service_data = self.manager.turn_on_event[entity_id].data[ATTR_SERVICE_DATA]
+ if self.manager._mark_manual_control_if_non_bare_turn_on(
+ entity_id,
+ service_data,
+ ):
+ _LOGGER.debug(
+ "Skipping responding to 'off' → 'on' event for '%s' with context.id='%s' because"
+ " we only adapt on bare `light.turn_on` events and not on service_data: '%s'",
+ entity_id,
+ event.context.id,
+ service_data,
+ )
+ return
+
if self._adapt_delay > 0:
await asyncio.sleep(self._adapt_delay)
@@ -2070,6 +2094,14 @@ class AdaptiveLightingManager:
# and of TOGGLE calls when toggling off.
or self.hass.states.is_state(entity_id, STATE_ON)
or self.manual_control.get(entity_id, False)
+ or (
+ switch._take_over_control
+ and switch._adapt_only_on_bare_turn_on
+ and self._mark_manual_control_if_non_bare_turn_on(
+ entity_id,
+ data[CONF_PARAMS],
+ )
+ )
):
_LOGGER.debug(
"Switch is off or light is already on for entity_id='%s', skipped='%s'"
@@ -2622,6 +2654,7 @@ class AdaptiveLightingManager:
turn_on_event = self.turn_on_event.get(light)
if (
turn_on_event is not None
+ and not self.is_proactively_adapting(turn_on_event.context.id)
and not is_our_context(turn_on_event.context)
and not force
):
@@ -2855,6 +2888,21 @@ class AdaptiveLightingManager:
)
return False
+ def _mark_manual_control_if_non_bare_turn_on(
+ self,
+ entity_id: str,
+ service_data: ServiceData,
+ ) -> bool:
+ _LOGGER.debug(
+ "_mark_manual_control_if_non_bare_turn_on: entity_id='%s', service_data='%s'",
+ entity_id,
+ service_data,
+ )
+ if any(attr in service_data for attr in COLOR_ATTRS | BRIGHTNESS_ATTRS):
+ self.mark_as_manual_control(entity_id)
+ return True
+ return False
+
class _AsyncSingleShotTimer:
def __init__(self, delay, callback) -> None:
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index c8e2c413..cbf87f96 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -46,10 +46,11 @@
"brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
"brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
"brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
- "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
"autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
"send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
"adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 1a25c7a1..692f0fde 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -88,6 +88,7 @@ from custom_components.adaptive_lighting.const import (
SERVICE_CHANGE_SWITCH_SETTINGS,
SERVICE_SET_MANUAL_CONTROL,
SLEEP_MODE_SWITCH,
+ CONF_ADAPT_ONLY_ON_BARE_TURN_ON,
UNDO_UPDATE_LISTENER,
)
from custom_components.adaptive_lighting.switch import (
@@ -561,9 +562,19 @@ async def test_manager_not_tracking_untracked_lights(hass):
assert light not in switch.manager.lights
-async def test_manual_control(hass):
+@pytest.mark.parametrize("adapt_only_on_bare_turn_on", [True, False])
+@pytest.mark.parametrize("proactive_service_call_adaptation", [True, False])
+async def test_manual_control(
+ hass, adapt_only_on_bare_turn_on, proactive_service_call_adaptation
+):
"""Test the 'manual control' tracking."""
- switch, (light, *_) = await setup_lights_and_switch(hass)
+ switch, (light, *_) = await setup_lights_and_switch(
+ hass,
+ {
+ CONF_ADAPT_ONLY_ON_BARE_TURN_ON: adapt_only_on_bare_turn_on,
+ INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation,
+ },
+ )
assert switch._take_over_control
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
@@ -639,9 +650,14 @@ async def test_manual_control(hass):
await change_manual_control(True)
assert manual_control[ENTITY_LIGHT_1]
await turn_light(False)
+ assert not manual_control[ENTITY_LIGHT_1], manual_control
await turn_light(True, brightness=increased_brightness())
assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
- assert not manual_control[ENTITY_LIGHT_1], manual_control
+ if adapt_only_on_bare_turn_on:
+ # Marks as manually controlled beacuse we turned it on with brightness
+ assert manual_control[ENTITY_LIGHT_1], manual_control
+ else:
+ assert not manual_control[ENTITY_LIGHT_1], manual_control
# Check that toggling (sleep mode) switch resets manual control
for entity_id in [ENTITY_SWITCH, ENTITY_SLEEP_MODE_SWITCH]:
From fec50ad5265064545dad62dddbb434d422b0da92 Mon Sep 17 00:00:00 2001
From: Benjamin Auquite
Date: Sat, 5 Aug 2023 23:19:18 -0500
Subject: [PATCH 101/549] Adapt lights simultaneously instead of one by one
(#529)
* Update switch.py
* Update test_switch.py
* Revert "Update test_switch.py"
This reverts commit 87ea5243b97bfa064c282382b29e09ed61917892.
* remove unnecessary create_task
* remove unneeded len()
* Revert "remove unnecessary create_task"
This reverts commit 2c5da6d73954e0ff3808b61092adbe55cdec1bb4.
* use `hass.async_create_task`
---------
Co-authored-by: Bas Nijholt
Co-authored-by: Bas Nijholt
---
custom_components/adaptive_lighting/switch.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 49d886fa..b0aa7f8c 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1399,7 +1399,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
adapt_color = self.adapt_color_switch.is_on
assert isinstance(adapt_brightness, bool)
assert isinstance(adapt_color, bool)
-
+ tasks = []
for light in filtered_lights:
manually_controlled = (
self._take_over_control
@@ -1446,7 +1446,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
transition,
context.id,
)
- await self._adapt_light(light, context, transition, force=force)
+ coro = self._adapt_light(light, context, transition, force=force)
+ task = self.hass.async_create_task(
+ coro,
+ )
+ tasks.append(task)
+ if tasks:
+ await asyncio.gather(*tasks)
async def _respond_to_off_to_on_event(self, entity_id: str, event: Event) -> None:
assert not self.manager.is_proactively_adapting(event.context.id)
From d2c6811e63af43135fc1bc4a5a3afea53c411461 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sat, 5 Aug 2023 23:06:27 -0700
Subject: [PATCH 102/549] Bump to 1.19.0b2 in manifest.json (#710)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 6202a765..cc91f7e2 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.19.0b1"
+ "version": "1.19.0b2"
}
From cb9ae39ee4693a82deeb5e2b2bc80300fada7401 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 6 Aug 2023 11:15:14 -0700
Subject: [PATCH 103/549] Fix adaptive_lighting.change_switch_settings service
(#712)
* Fix adaptive_lighting.change_switch_settings service
Closes #623
* filter defaults
* fix mutable
* Revert debugging logs
---
custom_components/adaptive_lighting/switch.py | 21 ++++++++++++-------
tests/test_switch.py | 6 ++++++
2 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index b0aa7f8c..88d94009 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -363,19 +363,19 @@ async def handle_change_switch_settings(
) -> None:
"""Allows HASS to change config values via a service call."""
data = service_call.data
-
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}
+ defaults = None
elif which == "configuration":
# use whatever's in the config flow or configuration.yaml
- defaults = switch._config_backup # pylint: disable=protected-access
+ defaults = switch._config_backup
else:
defaults = None
- switch._set_changeable_settings(data=data, defaults=defaults)
+ # deep copy the defaults so we don't modify the original dicts
+ switch._set_changeable_settings(data=data, defaults=deepcopy(defaults))
switch._update_time_interval_listener()
_LOGGER.debug(
@@ -589,7 +589,7 @@ def validate(
if defaults is None:
data = {key: default for key, default, _ in VALIDATION_TUPLES}
else:
- data = defaults
+ data = deepcopy(defaults)
if config_entry is not None:
assert service_data is None
@@ -598,7 +598,12 @@ def validate(
data.update(config_entry.data) # all yaml settings come from data
else:
assert service_data is not None
- data.update(service_data)
+ changed_settings = {
+ key: value
+ for key, value in service_data.items()
+ if key not in (CONF_USE_DEFAULTS, ATTR_ENTITY_ID)
+ }
+ data.update(changed_settings)
data = {key: replace_none_str(value) for key, value in data.items()}
for key, (validate_value, _) in EXTRA_VALIDATION.items():
value = data.get(key)
@@ -843,8 +848,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
def _set_changeable_settings(
self,
- data: dict,
- defaults: dict | None = None,
+ data: dict[str, Any],
+ defaults: dict[str, Any] | None = None,
):
# Only pass settings users can change during runtime
data = validate(
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 692f0fde..79dc9b20 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -1353,6 +1353,12 @@ async def test_change_switch_settings_service(hass):
await change_switch_settings(**{CONF_USE_DEFAULTS: "current"})
assert switch._sun_light_settings.min_color_temp == 2000
+ # testing with "configuration" and setting a new value
+ await change_switch_settings(
+ **{CONF_USE_DEFAULTS: "configuration", CONF_MIN_COLOR_TEMP: 3000}
+ )
+ assert switch._sun_light_settings.min_color_temp == 3000
+
# testing with "configuration" should revert back to 2500
await change_switch_settings(**{CONF_USE_DEFAULTS: "configuration"})
assert switch._sun_light_settings.min_color_temp == 2500
From 104664d3588442730f7ea9e811dd1986aca0c2e8 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 6 Aug 2023 11:21:54 -0700
Subject: [PATCH 104/549] Bump to 1.19.0b3 in manifest.json (#713)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index cc91f7e2..7a561884 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.19.0b2"
+ "version": "1.19.0b3"
}
From 943a6360c798dfbf59a348e21bb68b0316c078c3 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 6 Aug 2023 16:42:56 -0700
Subject: [PATCH 105/549] Add simple webapp to play with different parameters
(#715)
* Add simple webapp to play with different parameters
* ignore
---
.github/workflows/deploy-webapp.yml | 61 +++++++
.ruff.toml | 1 +
webapp/app.py | 268 ++++++++++++++++++++++++++++
webapp/requirements.txt | 1 +
4 files changed, 331 insertions(+)
create mode 100644 .github/workflows/deploy-webapp.yml
create mode 100644 webapp/app.py
create mode 100644 webapp/requirements.txt
diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml
new file mode 100644
index 00000000..7c4ac13e
--- /dev/null
+++ b/.github/workflows/deploy-webapp.yml
@@ -0,0 +1,61 @@
+# Simple workflow for deploying WebAssembly app to GitHub Pages
+name: Deploy WebAssembly app to Pages
+
+on:
+ # Runs on pushes targeting the default branch
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["main"]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
+# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ # Single deploy job since we're just deploying
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Set Up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.x
+
+ - name: Install Dependencies
+ run: |
+ pip install -r webapp/requirements.txt
+
+ - name: Build the WebAssembly app
+ run: |
+ shinylive export webapp site
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v3
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v2
+ with:
+ # Upload the 'site' directory, where your app has been built
+ path: "site"
+
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v2
diff --git a/.ruff.toml b/.ruff.toml
index 1cfeeb74..ead4d843 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -26,6 +26,7 @@ ignore = [
[per-file-ignores]
"tests/*.py" = ["ALL"]
".github/*py" = ["INP001"]
+"webapp/*py" = ["ALL"]
[flake8-pytest-style]
fixture-parentheses = false
diff --git a/webapp/app.py b/webapp/app.py
new file mode 100644
index 00000000..f99369cb
--- /dev/null
+++ b/webapp/app.py
@@ -0,0 +1,268 @@
+"""Simple web app to visualize brightness over time."""
+
+import math
+
+import matplotlib.pyplot as plt
+import numpy as np
+from shiny import App, render, ui
+
+
+def lerp(x, x1, x2, y1, y2):
+ """Linearly interpolate between two values."""
+ return y1 + (x - x1) * (y2 - y1) / (x2 - x1)
+
+
+def clamp(value: float, minimum: float, maximum: float) -> float:
+ """Clamp value between minimum and maximum."""
+ return max(minimum, min(value, maximum))
+
+
+def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]:
+ a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1)
+ b = x1 - (math.atanh(2 * y1 - 1) / a)
+ return a, b
+
+
+def scaled_tanh(
+ x: float,
+ a: float,
+ b: float,
+ y_min: float = 0.0,
+ y_max: float = 1.0,
+) -> float:
+ """Apply a scaled and shifted tanh function to a given input."""
+ return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1)
+
+
+def is_closer_to_sunrise_than_sunset(time, sunrise_time, sunset_time):
+ """Return True if the time is closer to sunrise than sunset."""
+ return abs(time - sunrise_time) < abs(time - sunset_time)
+
+
+def brightness_linear(
+ time,
+ sunrise_time,
+ sunset_time,
+ time_light,
+ time_dark,
+ max_brightness,
+ min_brightness,
+):
+ """Calculate the brightness for the 'linear' mode."""
+ closer_to_sunrise = is_closer_to_sunrise_than_sunset(
+ time,
+ sunrise_time,
+ sunset_time,
+ )
+ if closer_to_sunrise:
+ brightness = lerp(
+ time,
+ x1=sunrise_time - time_dark,
+ x2=sunrise_time + time_light,
+ y1=min_brightness,
+ y2=max_brightness,
+ )
+ else:
+ brightness = lerp(
+ time,
+ x1=sunset_time - time_light,
+ x2=sunset_time + time_dark,
+ y1=max_brightness,
+ y2=min_brightness,
+ )
+ return clamp(brightness, min_brightness, max_brightness)
+
+
+def brightness_tanh(
+ time,
+ sunrise_time,
+ sunset_time,
+ time_light,
+ time_dark,
+ max_brightness,
+ min_brightness,
+):
+ """Calculate the brightness for the 'tanh' mode."""
+ closer_to_sunrise = is_closer_to_sunrise_than_sunset(
+ time,
+ sunrise_time,
+ sunset_time,
+ )
+ if closer_to_sunrise:
+ a, b = find_a_b(
+ x1=-time_dark,
+ x2=time_light,
+ y1=0.05, # be at 5% of range at x1
+ y2=0.95, # be at 95% of range at x2
+ )
+ brightness = scaled_tanh(
+ time - sunrise_time,
+ a=a,
+ b=b,
+ y_min=min_brightness,
+ y_max=max_brightness,
+ )
+ else:
+ a, b = find_a_b(
+ x1=-time_light, # shifted timestamp for the start of sunset
+ x2=time_dark, # shifted timestamp for the end of sunset
+ y1=0.95, # be at 95% of range at the start of sunset
+ y2=0.05, # be at 5% of range at the end of sunset
+ )
+ brightness = scaled_tanh(
+ time - sunset_time,
+ a=a,
+ b=b,
+ y_min=min_brightness,
+ y_max=max_brightness,
+ )
+ return clamp(brightness, min_brightness, max_brightness)
+
+
+SEC_PER_HR = 60 * 60
+
+# Shiny UI
+app_ui = ui.page_fluid(
+ ui.layout_sidebar(
+ ui.panel_sidebar(
+ ui.input_slider("min_brightness", "min_brightness", 0, 100, 30, post="%"),
+ ui.input_slider("max_brightness", "max_brightness", 0, 100, 100, post="%"),
+ ui.input_slider(
+ "dark_time",
+ "brightness_mode_time_dark",
+ 0,
+ 5 * SEC_PER_HR,
+ 3 * SEC_PER_HR,
+ post=" sec",
+ ),
+ ui.input_slider(
+ "light_time",
+ "brightness_mode_time_light",
+ 0,
+ 5 * SEC_PER_HR,
+ 0.5 * SEC_PER_HR,
+ post=" sec",
+ ),
+ ui.input_slider(
+ "sunrise_time",
+ "sunrise_time",
+ 0,
+ 24,
+ 6,
+ step=0.5,
+ post=" hr",
+ ),
+ ui.input_slider(
+ "sunset_time",
+ "sunset_time",
+ 0,
+ 24,
+ 18,
+ step=0.5,
+ post=" hr",
+ ),
+ ),
+ ui.panel_main(ui.output_plot(id="brightness_plot")),
+ ),
+)
+
+
+def server(input, output, session):
+ @output
+ @render.plot
+ def brightness_plot():
+ return plot_brightness(
+ min_brightness=input.min_brightness() / 100,
+ max_brightness=input.max_brightness() / 100,
+ brightness_mode_time_dark=input.dark_time() / SEC_PER_HR,
+ brightness_mode_time_light=input.light_time() / SEC_PER_HR,
+ sunrise_time=input.sunrise_time(),
+ sunset_time=input.sunset_time(),
+ )
+
+
+def plot_brightness(
+ min_brightness,
+ max_brightness,
+ brightness_mode_time_dark,
+ brightness_mode_time_light,
+ sunrise_time=6, # 6 AM
+ sunset_time=18, # 6 PM
+):
+ # Define the time range for our simulation
+ time_range = np.linspace(0, 24, 1000) # From 0 to 24 hours
+
+ # Calculate the brightness for each time in the time range for both modes
+ brightness_linear_values = [
+ brightness_linear(
+ time,
+ sunrise_time,
+ sunset_time,
+ brightness_mode_time_light,
+ brightness_mode_time_dark,
+ max_brightness,
+ min_brightness,
+ )
+ for time in time_range
+ ]
+ brightness_tanh_values = [
+ brightness_tanh(
+ time,
+ sunrise_time,
+ sunset_time,
+ brightness_mode_time_light,
+ brightness_mode_time_dark,
+ max_brightness,
+ min_brightness,
+ )
+ for time in time_range
+ ]
+
+ # Plot the brightness over time for both modes
+ plt.figure(figsize=(10, 6))
+ plt.plot(time_range, brightness_linear_values, label="Linear Mode")
+ plt.plot(time_range, brightness_tanh_values, label="Tanh Mode")
+ plt.vlines(sunrise_time, 0, 1, color="C2", label="Sunrise", linestyles="dashed")
+ plt.vlines(sunset_time, 0, 1, color="C3", label="Sunset", linestyles="dashed")
+ plt.xlim(0, 24)
+ plt.xticks(np.arange(0, 25, 1))
+ yticks = np.arange(0, 1.05, 0.05)
+ ytick_labels = [f"{100*label:.0f}%" for label in yticks]
+ plt.yticks(yticks, ytick_labels)
+ plt.xlabel("Time (hours)")
+ plt.ylabel("Brightness")
+ plt.title("Brightness over Time for Different Modes")
+
+ # Add text box
+ textstr = "\n".join(
+ (
+ f"Sunrise Time = {sunrise_time}:00:00",
+ f"Sunset Time = {sunset_time}:00:00",
+ f"Max Brightness = {max_brightness*100:.0f}%",
+ f"Min Brightness = {min_brightness*100:.0f}%",
+ f"Time Light = {brightness_mode_time_light:.1f} hours",
+ f"Time Dark = {brightness_mode_time_dark:.1f} hours",
+ ),
+ )
+
+ # these are matplotlib.patch.Patch properties
+ props = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}
+
+ plt.legend()
+ plt.grid(True)
+
+ # place a text box in upper left in axes coords
+ plt.gca().text(
+ 0.4,
+ 0.55,
+ textstr,
+ transform=plt.gca().transAxes,
+ fontsize=10,
+ verticalalignment="center",
+ bbox=props,
+ )
+
+ return plt.gcf()
+
+
+app = App(app_ui, server)
diff --git a/webapp/requirements.txt b/webapp/requirements.txt
new file mode 100644
index 00000000..f832a57a
--- /dev/null
+++ b/webapp/requirements.txt
@@ -0,0 +1 @@
+shinylive
From 69355b8c3a50ebf00d0b02924f162d81d9562a75 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 6 Aug 2023 17:20:58 -0700
Subject: [PATCH 106/549] Small WebApp improvements (#716)
* Small WebApp improvements
* Add link
---
webapp/app.py | 140 ++++++++++++++++++++++++++++----------------------
1 file changed, 78 insertions(+), 62 deletions(-)
diff --git a/webapp/app.py b/webapp/app.py
index f99369cb..208ea242 100644
--- a/webapp/app.py
+++ b/webapp/app.py
@@ -119,68 +119,6 @@ def brightness_tanh(
return clamp(brightness, min_brightness, max_brightness)
-SEC_PER_HR = 60 * 60
-
-# Shiny UI
-app_ui = ui.page_fluid(
- ui.layout_sidebar(
- ui.panel_sidebar(
- ui.input_slider("min_brightness", "min_brightness", 0, 100, 30, post="%"),
- ui.input_slider("max_brightness", "max_brightness", 0, 100, 100, post="%"),
- ui.input_slider(
- "dark_time",
- "brightness_mode_time_dark",
- 0,
- 5 * SEC_PER_HR,
- 3 * SEC_PER_HR,
- post=" sec",
- ),
- ui.input_slider(
- "light_time",
- "brightness_mode_time_light",
- 0,
- 5 * SEC_PER_HR,
- 0.5 * SEC_PER_HR,
- post=" sec",
- ),
- ui.input_slider(
- "sunrise_time",
- "sunrise_time",
- 0,
- 24,
- 6,
- step=0.5,
- post=" hr",
- ),
- ui.input_slider(
- "sunset_time",
- "sunset_time",
- 0,
- 24,
- 18,
- step=0.5,
- post=" hr",
- ),
- ),
- ui.panel_main(ui.output_plot(id="brightness_plot")),
- ),
-)
-
-
-def server(input, output, session):
- @output
- @render.plot
- def brightness_plot():
- return plot_brightness(
- min_brightness=input.min_brightness() / 100,
- max_brightness=input.max_brightness() / 100,
- brightness_mode_time_dark=input.dark_time() / SEC_PER_HR,
- brightness_mode_time_light=input.light_time() / SEC_PER_HR,
- sunrise_time=input.sunrise_time(),
- sunset_time=input.sunset_time(),
- )
-
-
def plot_brightness(
min_brightness,
max_brightness,
@@ -265,4 +203,82 @@ def plot_brightness(
return plt.gcf()
+SEC_PER_HR = 60 * 60
+desc = """
+**Experience the Dynamics of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) in Real-Time.**
+
+Have you ever wondered how the intricate settings of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) impact your home ambiance? The Adaptive Lighting Simulator WebApp is here to demystify just that.
+
+Harnessing the technology of the popular Adaptive Lighting integration for Home Assistant, this webapp provides a hands-on, visual platform to explore, tweak, and understand the myriad of parameters that dictate the behavior of your smart lights. Whether you're aiming for a subtle morning glow or a cozy evening warmth, observe firsthand how each tweak changes the ambiance.
+
+**Why Use the Simulator?**
+- **Interactive Exploration**: No more guesswork. See in real-time how changes to settings influence the lighting dynamics.
+- **Circadian Cycle Preview**: Understand how Adaptive Lighting adjusts throughout the day based on specific parameters, ensuring your lighting aligns with your circadian rhythms.
+- **Tailored Testing**: Play with parameters and find the perfect combination that suits your personal or family's needs.
+- **Educational Experience**: For both newbies and experts, delve deep into the intricacies of Adaptive Lighting's logic and potential.
+
+Dive into the simulator, experiment with different settings, and fine-tune the behavior of Adaptive Lighting to perfection. Whether you're setting it up for the first time or optimizing an existing setup, this tool ensures you get the most out of your smart lighting experience.
+"""
+
+# Shiny UI
+app_ui = ui.page_fluid(
+ ui.panel_title("🌞 Adaptive Lighting Simulator WebApp 🌛"),
+ ui.layout_sidebar(
+ ui.panel_sidebar(
+ ui.input_slider("min_brightness", "min_brightness", 0, 100, 30, post="%"),
+ ui.input_slider("max_brightness", "max_brightness", 0, 100, 100, post="%"),
+ ui.input_slider(
+ "dark_time",
+ "brightness_mode_time_dark",
+ 0,
+ 5 * SEC_PER_HR,
+ 3 * SEC_PER_HR,
+ post=" sec",
+ ),
+ ui.input_slider(
+ "light_time",
+ "brightness_mode_time_light",
+ 0,
+ 5 * SEC_PER_HR,
+ 0.5 * SEC_PER_HR,
+ post=" sec",
+ ),
+ ui.input_slider(
+ "sunrise_time",
+ "sunrise_time",
+ 0,
+ 24,
+ 6,
+ step=0.5,
+ post=" hr",
+ ),
+ ui.input_slider(
+ "sunset_time",
+ "sunset_time",
+ 0,
+ 24,
+ 18,
+ step=0.5,
+ post=" hr",
+ ),
+ ),
+ ui.panel_main(ui.markdown(desc), ui.output_plot(id="brightness_plot")),
+ ),
+)
+
+
+def server(input, output, session):
+ @output
+ @render.plot
+ def brightness_plot():
+ return plot_brightness(
+ min_brightness=input.min_brightness() / 100,
+ max_brightness=input.max_brightness() / 100,
+ brightness_mode_time_dark=input.dark_time() / SEC_PER_HR,
+ brightness_mode_time_light=input.light_time() / SEC_PER_HR,
+ sunrise_time=input.sunrise_time(),
+ sunset_time=input.sunset_time(),
+ )
+
+
app = App(app_ui, server)
From ef1546b6b93a47b51aa6f1379b938cbc071ad3dd Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 6 Aug 2023 19:21:16 -0700
Subject: [PATCH 107/549] Fix min_sunrise_time and max_sunset_time (#717)
Closes #714
---
custom_components/adaptive_lighting/const.py | 2 ++
custom_components/adaptive_lighting/switch.py | 2 ++
2 files changed, 4 insertions(+)
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index f3dc628d..0608d3da 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -377,10 +377,12 @@ EXTRA_VALIDATION = {
CONF_INTERVAL: (cv.time_period, timedelta_as_int),
CONF_SUNRISE_OFFSET: (cv.time_period, timedelta_as_int),
CONF_SUNRISE_TIME: (cv.time, str),
+ CONF_MIN_SUNRISE_TIME: (cv.time, str),
CONF_MAX_SUNRISE_TIME: (cv.time, str),
CONF_SUNSET_OFFSET: (cv.time_period, timedelta_as_int),
CONF_SUNSET_TIME: (cv.time, str),
CONF_MIN_SUNSET_TIME: (cv.time, str),
+ CONF_MAX_SUNSET_TIME: (cv.time, str),
CONF_BRIGHTNESS_MODE_TIME_LIGHT: (cv.time_period, timedelta_as_int),
CONF_BRIGHTNESS_MODE_TIME_DARK: (cv.time_period, timedelta_as_int),
}
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 88d94009..07eb0a97 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1689,8 +1689,10 @@ class SunLightSettings:
if (
self.sunrise_time is None
and self.sunset_time is None
+ and self.min_sunrise_time is None
and self.max_sunrise_time is None
and self.min_sunset_time is None
+ and self.max_sunset_time is None
):
solar_noon = location.noon(date, local=False)
solar_midnight = location.midnight(date, local=False)
From 00ec76a4678ba94cbb8855a06d7418954dc7530e Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 6 Aug 2023 19:21:39 -0700
Subject: [PATCH 108/549] Bump to 1.19.0b4 in manifest.json (#718)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 7a561884..3b6c7cf5 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.19.0b3"
+ "version": "1.19.0b4"
}
From 61d51cbf1e6486029b66253906248d27c473fbd6 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 7 Aug 2023 22:20:28 -0700
Subject: [PATCH 109/549] [pre-commit.ci] pre-commit autoupdate (#722)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.281 → v0.0.282](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.281...v0.0.282)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6e27d970..31fdf86c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,7 +8,7 @@ repos:
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.0.281
+ rev: v0.0.282
hooks:
- id: ruff
args: ["--fix"]
From f8e7880a968651e5d4bbede8dacfec8e20230052 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 8 Aug 2023 14:31:17 -0700
Subject: [PATCH 110/549] Split up SunLightSettings and improve
https://basnijholt.github.io/adaptive-lighting/ (#719)
* Split up SunLightSettings
* Renames
* factor out SunEvents
* more renames
* rewrite
* rewrite more
* simpler
* refactor
* refact
* raise
* refact
* rename
* move method
* clean
* Move to new module 'sun.py'
* make sun independent of HA
* rename
* Move to webapp/homeassistant_util_color.py
* Rework app
* Add link
* new plotting
* app changes
* fix tests
* test clean
* tz fixes
* fix
* use sed
* verbose
* fix tz
* fix
* tiem
---
.github/workflows/deploy-webapp.yml | 3 +
.ruff.toml | 1 +
README.md | 2 +-
.../adaptive_lighting/color_and_brightness.py | 518 ++++++++++++
custom_components/adaptive_lighting/const.py | 2 -
.../adaptive_lighting/helpers.py | 122 ---
custom_components/adaptive_lighting/switch.py | 314 +------
tests/test_color_and_brightness.py | 209 +++++
tests/test_switch.py | 23 +-
webapp/app.py | 400 ++++-----
webapp/homeassistant_util_color.py | 773 ++++++++++++++++++
webapp/requirements.txt | 1 +
12 files changed, 1744 insertions(+), 624 deletions(-)
create mode 100644 custom_components/adaptive_lighting/color_and_brightness.py
create mode 100644 tests/test_color_and_brightness.py
create mode 100644 webapp/homeassistant_util_color.py
diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml
index 7c4ac13e..04504522 100644
--- a/.github/workflows/deploy-webapp.yml
+++ b/.github/workflows/deploy-webapp.yml
@@ -45,6 +45,9 @@ jobs:
- name: Build the WebAssembly app
run: |
+ set -ex
+ cp custom_components/adaptive_lighting/color_and_brightness.py webapp/color_and_brightness.py
+ sed -i 's/homeassistant.util.color/homeassistant_util_color/g' "webapp/color_and_brightness.py"
shinylive export webapp site
- name: Setup Pages
diff --git a/.ruff.toml b/.ruff.toml
index ead4d843..8ece9b9c 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -27,6 +27,7 @@ ignore = [
"tests/*.py" = ["ALL"]
".github/*py" = ["INP001"]
"webapp/*py" = ["ALL"]
+"custom_components/adaptive_lighting/homeassistant_util_color.py" = ["ALL"]
[flake8-pytest-style]
fixture-parentheses = false
diff --git a/README.md b/README.md
index 56d9f05d..faa43e9b 100644
--- a/README.md
+++ b/README.md
@@ -441,7 +441,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark


-> [*Code to make the plots*](https://github.com/basnijholt/adaptive-lighting/pull/699#issuecomment-1666232555)
+> Check out the interactive webapp on https://basnijholt.github.io/adaptive-lighting/ to play with the parameters and see how the brightness changes!
## :eyes: See also
diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py
new file mode 100644
index 00000000..2968fcc0
--- /dev/null
+++ b/custom_components/adaptive_lighting/color_and_brightness.py
@@ -0,0 +1,518 @@
+"""Switch for the Adaptive Lighting integration."""
+from __future__ import annotations
+
+import bisect
+import colorsys
+import datetime
+import logging
+import math
+from dataclasses import dataclass
+from datetime import timedelta
+from functools import cached_property, partial
+from typing import TYPE_CHECKING, Any, Literal, cast
+
+from homeassistant.util.color import (
+ color_RGB_to_xy,
+ color_temperature_to_rgb,
+ color_xy_to_hs,
+)
+
+if TYPE_CHECKING:
+ import astral
+
+# Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET
+# We re-define them here to not depend on homeassistant in this file.
+SUN_EVENT_SUNRISE = "sunrise"
+SUN_EVENT_SUNSET = "sunset"
+
+SUN_EVENT_NOON = "solar_noon"
+SUN_EVENT_MIDNIGHT = "solar_midnight"
+
+_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT)
+_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))}
+
+UTC = datetime.timezone.utc
+utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC)
+utcnow.__doc__ = "Get now in UTC time."
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class SunEvents:
+ """Track the state of the sun and associated light settings."""
+
+ name: str
+ astral_location: astral.Location
+ sunrise_time: datetime.time | None
+ min_sunrise_time: datetime.time | None
+ max_sunrise_time: datetime.time | None
+ sunset_time: datetime.time | None
+ min_sunset_time: datetime.time | None
+ max_sunset_time: datetime.time | None
+ sunrise_offset: datetime.timedelta = datetime.timedelta()
+ sunset_offset: datetime.timedelta = datetime.timedelta()
+ timezone: datetime.tzinfo = UTC
+
+ def sunrise(self, dt: datetime.date) -> datetime.datetime:
+ """Return the (adjusted) sunrise time for the given datetime."""
+ sunrise = (
+ self.astral_location.sunrise(dt, local=False)
+ if self.sunrise_time is None
+ else self._replace_time(dt, self.sunrise_time)
+ ) + self.sunrise_offset
+ if self.min_sunrise_time is not None:
+ min_sunrise = self._replace_time(dt, self.min_sunrise_time)
+ if min_sunrise > sunrise:
+ sunrise = min_sunrise
+ if self.max_sunrise_time is not None:
+ max_sunrise = self._replace_time(dt, self.max_sunrise_time)
+ if max_sunrise < sunrise:
+ sunrise = max_sunrise
+ return sunrise
+
+ def sunset(self, dt: datetime.date) -> datetime.datetime:
+ """Return the (adjusted) sunset time for the given datetime."""
+ sunset = (
+ self.astral_location.sunset(dt, local=False)
+ if self.sunset_time is None
+ else self._replace_time(dt, self.sunset_time)
+ ) + self.sunset_offset
+ if self.min_sunset_time is not None:
+ min_sunset = self._replace_time(dt, self.min_sunset_time)
+ if min_sunset > sunset:
+ sunset = min_sunset
+ if self.max_sunset_time is not None:
+ max_sunset = self._replace_time(dt, self.max_sunset_time)
+ if max_sunset < sunset:
+ sunset = max_sunset
+ return sunset
+
+ def _replace_time(
+ self,
+ dt: datetime.date,
+ time: datetime.time,
+ ) -> datetime.datetime:
+ date_time = datetime.datetime.combine(dt, time)
+ dt_with_tz = date_time.replace(tzinfo=self.timezone)
+ return dt_with_tz.astimezone(UTC)
+
+ def noon_and_midnight(
+ self,
+ dt: datetime.datetime,
+ sunset: datetime.datetime | None = None,
+ sunrise: datetime.datetime | None = None,
+ ) -> tuple[datetime.datetime, datetime.datetime]:
+ """Return the (adjusted) noon and midnight times for the given datetime."""
+ if (
+ self.sunrise_time is None
+ and self.sunset_time is None
+ and self.min_sunrise_time is None
+ and self.max_sunrise_time is None
+ and self.min_sunset_time is None
+ and self.max_sunset_time is None
+ ):
+ solar_noon = self.astral_location.noon(dt, local=False)
+ solar_midnight = self.astral_location.midnight(dt, local=False)
+ return solar_noon, solar_midnight
+
+ if sunset is None:
+ sunset = self.sunset(dt)
+ if sunrise is None:
+ sunrise = self.sunrise(dt)
+
+ middle = abs(sunset - sunrise) / 2
+ if sunset > sunrise:
+ noon = sunrise + middle
+ midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1)
+ else:
+ midnight = sunset + middle
+ noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1)
+ return noon, midnight
+
+ def sun_events(self, dt: datetime.datetime) -> list[tuple[str, float]]:
+ """Get the four sun event's timestamps at 'dt'."""
+ sunrise = self.sunrise(dt)
+ sunset = self.sunset(dt)
+ solar_noon, solar_midnight = self.noon_and_midnight(dt, sunset, sunrise)
+ events = [
+ (SUN_EVENT_SUNRISE, sunrise.timestamp()),
+ (SUN_EVENT_SUNSET, sunset.timestamp()),
+ (SUN_EVENT_NOON, solar_noon.timestamp()),
+ (SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()),
+ ]
+ self._validate_sun_event_order(events)
+ return events
+
+ def _validate_sun_event_order(self, events: list[tuple[str, float]]) -> None:
+ """Check if the sun events are in the expected order."""
+ events = sorted(events, key=lambda x: x[1])
+ events_names, _ = zip(*events, strict=True)
+ if events_names not in _ALLOWED_ORDERS:
+ msg = (
+ f"{self.name}: The sun events {events_names} are not in the expected"
+ " order. The Adaptive Lighting integration will not work!"
+ " This might happen if your sunrise/sunset offset is too large or"
+ " your manually set sunrise/sunset time is past/before noon/midnight."
+ )
+ _LOGGER.error(msg)
+ raise ValueError(msg)
+
+ def prev_and_next_events(self, dt: datetime.datetime) -> list[tuple[str, float]]:
+ """Get the previous and next sun event."""
+ events = [
+ event
+ for days in [-1, 0, 1]
+ for event in self.sun_events(dt + timedelta(days=days))
+ ]
+ events = sorted(events, key=lambda x: x[1])
+ i_now = bisect.bisect([ts for _, ts in events], dt.timestamp())
+ return events[i_now - 1 : i_now + 1]
+
+ def sun_position(self, dt: datetime.datetime) -> float:
+ """Calculate the position of the sun, between [-1, 1]."""
+ target_ts = dt.timestamp()
+ (_, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt)
+ h, x = (
+ (prev_ts, next_ts)
+ if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
+ else (next_ts, prev_ts)
+ )
+ # k = -1 between sunset and sunrise (sun below horizon)
+ # k = 1 between sunrise and sunset (sun above horizon)
+ k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1
+ return k * (1 - ((target_ts - h) / (h - x)) ** 2)
+
+ def closest_event(self, dt: datetime.datetime) -> tuple[str, float]:
+ """Get the closest sunset or sunrise event."""
+ (prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt)
+ if prev_event == SUN_EVENT_SUNRISE or next_event == SUN_EVENT_SUNRISE:
+ ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts
+ return SUN_EVENT_SUNRISE, ts_event
+ if prev_event == SUN_EVENT_SUNSET or next_event == SUN_EVENT_SUNSET:
+ ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts
+ return SUN_EVENT_SUNSET, ts_event
+ msg = "No sunrise or sunset event found."
+ raise ValueError(msg)
+
+
+@dataclass(frozen=True)
+class SunLightSettings:
+ """Track the state of the sun and associated light settings."""
+
+ name: str
+ astral_location: astral.Location
+ adapt_until_sleep: bool
+ max_brightness: int
+ max_color_temp: int
+ min_brightness: int
+ min_color_temp: int
+ sleep_brightness: int
+ sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"]
+ sleep_color_temp: int
+ sleep_rgb_color: tuple[int, int, int]
+ sunrise_time: datetime.time | None
+ min_sunrise_time: datetime.time | None
+ max_sunrise_time: datetime.time | None
+ sunset_time: datetime.time | None
+ min_sunset_time: datetime.time | None
+ max_sunset_time: datetime.time | None
+ brightness_mode_time_dark: datetime.timedelta
+ brightness_mode_time_light: datetime.timedelta
+ brightness_mode: Literal["default", "linear", "tanh"] = "default"
+ sunrise_offset: datetime.timedelta = datetime.timedelta()
+ sunset_offset: datetime.timedelta = datetime.timedelta()
+ timezone: datetime.tzinfo = UTC
+
+ @cached_property
+ def sun(self) -> SunEvents:
+ """Return the SunEvents object."""
+ return SunEvents(
+ name=self.name,
+ astral_location=self.astral_location,
+ sunrise_time=self.sunrise_time,
+ sunrise_offset=self.sunrise_offset,
+ min_sunrise_time=self.min_sunrise_time,
+ max_sunrise_time=self.max_sunrise_time,
+ sunset_time=self.sunset_time,
+ sunset_offset=self.sunset_offset,
+ min_sunset_time=self.min_sunset_time,
+ max_sunset_time=self.max_sunset_time,
+ timezone=self.timezone,
+ )
+
+ def _brightness_pct_default(self, dt: datetime.datetime) -> float:
+ """Calculate the brightness percentage using the default method."""
+ sun_position = self.sun.sun_position(dt)
+ if sun_position > 0:
+ return self.max_brightness
+ delta_brightness = self.max_brightness - self.min_brightness
+ return (delta_brightness * (1 + sun_position)) + self.min_brightness
+
+ def _brightness_pct_tanh(self, dt: datetime.datetime) -> float:
+ event, ts_event = self.sun.closest_event(dt)
+ dark = self.brightness_mode_time_dark.total_seconds()
+ light = self.brightness_mode_time_light.total_seconds()
+ if event == SUN_EVENT_SUNRISE:
+ brightness = scaled_tanh(
+ dt.timestamp() - ts_event,
+ x1=-dark,
+ x2=+light,
+ y1=0.05, # be at 5% of range at x1
+ y2=0.95, # be at 95% of range at x2
+ y_min=self.min_brightness,
+ y_max=self.max_brightness,
+ )
+ elif event == SUN_EVENT_SUNSET:
+ brightness = scaled_tanh(
+ dt.timestamp() - ts_event,
+ x1=-light, # shifted timestamp for the start of sunset
+ x2=+dark, # shifted timestamp for the end of sunset
+ y1=0.95, # be at 95% of range at the start of sunset
+ y2=0.05, # be at 5% of range at the end of sunset
+ y_min=self.min_brightness,
+ y_max=self.max_brightness,
+ )
+ return clamp(brightness, self.min_brightness, self.max_brightness)
+
+ def _brightness_pct_linear(self, dt: datetime.datetime) -> float:
+ event, ts_event = self.sun.closest_event(dt)
+ # at ts_event - dt_start, brightness == start_brightness
+ # at ts_event + dt_end, brightness == end_brightness
+ dark = self.brightness_mode_time_dark.total_seconds()
+ light = self.brightness_mode_time_light.total_seconds()
+ if event == SUN_EVENT_SUNRISE:
+ brightness = lerp(
+ dt.timestamp() - ts_event,
+ x1=-dark,
+ x2=+light,
+ y1=self.min_brightness,
+ y2=self.max_brightness,
+ )
+ elif event == SUN_EVENT_SUNSET:
+ brightness = lerp(
+ dt.timestamp() - ts_event,
+ x1=-light,
+ x2=+dark,
+ y1=self.max_brightness,
+ y2=self.min_brightness,
+ )
+ return clamp(brightness, self.min_brightness, self.max_brightness)
+
+ def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float:
+ """Calculate the brightness in %."""
+ if is_sleep:
+ return self.sleep_brightness
+ assert self.brightness_mode in ("default", "linear", "tanh")
+ if self.brightness_mode == "default":
+ return self._brightness_pct_default(dt)
+ if self.brightness_mode == "linear":
+ return self._brightness_pct_linear(dt)
+ if self.brightness_mode == "tanh":
+ return self._brightness_pct_tanh(dt)
+ return None
+
+ def color_temp_kelvin(self, sun_position: float) -> int:
+ """Calculate the color temperature in Kelvin."""
+ if sun_position > 0:
+ delta = self.max_color_temp - self.min_color_temp
+ ct = (delta * sun_position) + self.min_color_temp
+ return 5 * round(ct / 5) # round to nearest 5
+ if sun_position == 0 or not self.adapt_until_sleep:
+ return self.min_color_temp
+ if self.adapt_until_sleep and sun_position < 0:
+ delta = abs(self.min_color_temp - self.sleep_color_temp)
+ ct = (delta * abs(1 + sun_position)) + self.sleep_color_temp
+ return 5 * round(ct / 5) # round to nearest 5
+ msg = "Should not happen"
+ raise ValueError(msg)
+
+ def brightness_and_color(
+ self,
+ dt: datetime.datetime,
+ is_sleep: bool,
+ ) -> dict[str, Any]:
+ """Calculate the brightness and color."""
+ sun_position = self.sun.sun_position(dt)
+ rgb_color: tuple[float, float, float]
+ # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled)
+ force_rgb_color = False
+ brightness_pct = self.brightness_pct(dt, is_sleep)
+ if is_sleep:
+ color_temp_kelvin = self.sleep_color_temp
+ rgb_color = self.sleep_rgb_color
+ elif (
+ self.sleep_rgb_or_color_temp == "rgb_color"
+ and self.adapt_until_sleep
+ and sun_position < 0
+ ):
+ # Feature requested in
+ # https://github.com/basnijholt/adaptive-lighting/issues/624
+ # This will result in a perceptible jump in color at sunset and sunrise
+ # because the `color_temperature_to_rgb` function is not 100% accurate.
+ min_color_rgb = color_temperature_to_rgb(self.min_color_temp)
+ rgb_color = lerp_color_hsv(
+ min_color_rgb,
+ self.sleep_rgb_color,
+ sun_position,
+ )
+ color_temp_kelvin = self.color_temp_kelvin(sun_position)
+ force_rgb_color = True
+ else:
+ color_temp_kelvin = self.color_temp_kelvin(sun_position)
+ rgb_color = color_temperature_to_rgb(color_temp_kelvin)
+ # backwards compatibility for versions < 1.3.1 - see #403
+ color_temp_mired: float = math.floor(1000000 / color_temp_kelvin)
+ xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color)
+ hs_color: tuple[float, float] = color_xy_to_hs(*xy_color)
+ return {
+ "brightness_pct": brightness_pct,
+ "color_temp_kelvin": color_temp_kelvin,
+ "color_temp_mired": color_temp_mired,
+ "rgb_color": rgb_color,
+ "xy_color": xy_color,
+ "hs_color": hs_color,
+ "sun_position": sun_position,
+ "force_rgb_color": force_rgb_color,
+ }
+
+ def get_settings(
+ self,
+ is_sleep,
+ transition,
+ ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]:
+ """Get all light settings.
+
+ Calculating all values takes <0.5ms.
+ """
+ dt = utcnow() + timedelta(seconds=transition or 0)
+ return self.brightness_and_color(dt, is_sleep)
+
+
+def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]:
+ """Compute the values of 'a' and 'b' for a scaled and shifted tanh function.
+
+ Given two points (x1, y1) and (x2, y2), this function calculates the coefficients 'a' and 'b'
+ for a tanh function of the form y = 0.5 * (tanh(a * (x - b)) + 1) that passes through these points.
+
+ The derivation is as follows:
+
+ 1. Start with the equation of the tanh function:
+ y = 0.5 * (tanh(a * (x - b)) + 1)
+
+ 2. Rearrange the equation to isolate tanh:
+ tanh(a * (x - b)) = 2*y - 1
+
+ 3. Take the inverse tanh (or artanh) on both sides to solve for 'a' and 'b':
+ a * (x - b) = artanh(2*y - 1)
+
+ 4. Plug in the points (x1, y1) and (x2, y2) to get two equations.
+ Using these, we can solve for 'a' and 'b' as:
+ a = (artanh(2*y2 - 1) - artanh(2*y1 - 1)) / (x2 - x1)
+ b = x1 - (artanh(2*y1 - 1) / a)
+
+ Parameters
+ ----------
+ x1
+ x-coordinate of the first point.
+ x2
+ x-coordinate of the second point.
+ y1
+ y-coordinate of the first point (should be between 0 and 1).
+ y2
+ y-coordinate of the second point (should be between 0 and 1).
+
+ Returns
+ -------
+ a
+ Coefficient 'a' for the tanh function.
+ b
+ Coefficient 'b' for the tanh function.
+
+ Notes
+ -----
+ The values of y1 and y2 should lie between 0 and 1, inclusive.
+ """
+ a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1)
+ b = x1 - (math.atanh(2 * y1 - 1) / a)
+ return a, b
+
+
+def scaled_tanh(
+ x: float,
+ x1: float,
+ x2: float,
+ y1: float = 0.05,
+ y2: float = 0.95,
+ y_min: float = 0.0,
+ y_max: float = 100.0,
+) -> float:
+ """Apply a scaled and shifted tanh function to a given input.
+
+ This function represents a transformation of the tanh function that scales and shifts
+ the output to lie between y_min and y_max. For values of 'x' close to 'x1' and 'x2'
+ (used to calculate 'a' and 'b'), the output of this function will be close to 'y_min'
+ and 'y_max', respectively.
+
+ The equation of the function is as follows:
+ y = y_min + (y_max - y_min) * 0.5 * (tanh(a * (x - b)) + 1)
+
+ Parameters
+ ----------
+ x
+ The input to the function.
+ x1
+ x-coordinate of the first point.
+ x2
+ x-coordinate of the second point.
+ y1
+ y-coordinate of the first point (should be between 0 and 1). Defaults to 0.05.
+ y2
+ y-coordinate of the second point (should be between 0 and 1). Defaults to 0.95.
+ y_min
+ The minimum value of the output range. Defaults to 0.
+ y_max
+ The maximum value of the output range. Defaults to 100.
+
+ Returns
+ -------
+ float: The output of the function, which lies in the range [y_min, y_max].
+ """
+ a, b = find_a_b(x1, x2, y1, y2)
+ return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1)
+
+
+def lerp_color_hsv(
+ rgb1: tuple[float, float, float],
+ rgb2: tuple[float, float, float],
+ t: float,
+) -> tuple[int, int, int]:
+ """Linearly interpolate between two RGB colors in HSV color space."""
+ t = abs(t)
+ assert 0 <= t <= 1
+
+ # Convert RGB to HSV
+ hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1])
+ hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2])
+
+ # Linear interpolation in HSV space
+ hsv = (
+ hsv1[0] + t * (hsv2[0] - hsv1[0]),
+ hsv1[1] + t * (hsv2[1] - hsv1[1]),
+ hsv1[2] + t * (hsv2[2] - hsv1[2]),
+ )
+
+ # Convert back to RGB
+ rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv))
+ assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}"
+ return cast(tuple[int, int, int], rgb)
+
+
+def lerp(x, x1, x2, y1, y2):
+ """Linearly interpolate between two values."""
+ return y1 + (x - x1) * (y2 - y1) / (x2 - x1)
+
+
+def clamp(value: float, minimum: float, maximum: float) -> float:
+ """Clamp value between minimum and maximum."""
+ return max(minimum, min(value, maximum))
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index 0608d3da..c747528d 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -12,8 +12,6 @@ ICON_COLOR_TEMP = "mdi:sun-thermometer"
ICON_SLEEP = "mdi:sleep"
DOMAIN = "adaptive_lighting"
-SUN_EVENT_NOON = "solar_noon"
-SUN_EVENT_MIDNIGHT = "solar_midnight"
DOCS = {CONF_ENTITY_ID: "Entity ID of the switch. 📝"}
diff --git a/custom_components/adaptive_lighting/helpers.py b/custom_components/adaptive_lighting/helpers.py
index 95c87735..2e7ba23e 100644
--- a/custom_components/adaptive_lighting/helpers.py
+++ b/custom_components/adaptive_lighting/helpers.py
@@ -3,12 +3,7 @@
from __future__ import annotations
import base64
-import colorsys
-import logging
import math
-from typing import cast
-
-_LOGGER = logging.getLogger(__name__)
def clamp(value: float, minimum: float, maximum: float) -> float:
@@ -16,123 +11,6 @@ def clamp(value: float, minimum: float, maximum: float) -> float:
return max(minimum, min(value, maximum))
-def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]:
- """Compute the values of 'a' and 'b' for a scaled and shifted tanh function.
-
- Given two points (x1, y1) and (x2, y2), this function calculates the coefficients 'a' and 'b'
- for a tanh function of the form y = 0.5 * (tanh(a * (x - b)) + 1) that passes through these points.
-
- The derivation is as follows:
-
- 1. Start with the equation of the tanh function:
- y = 0.5 * (tanh(a * (x - b)) + 1)
-
- 2. Rearrange the equation to isolate tanh:
- tanh(a * (x - b)) = 2*y - 1
-
- 3. Take the inverse tanh (or artanh) on both sides to solve for 'a' and 'b':
- a * (x - b) = artanh(2*y - 1)
-
- 4. Plug in the points (x1, y1) and (x2, y2) to get two equations.
- Using these, we can solve for 'a' and 'b' as:
- a = (artanh(2*y2 - 1) - artanh(2*y1 - 1)) / (x2 - x1)
- b = x1 - (artanh(2*y1 - 1) / a)
-
- Parameters
- ----------
- x1
- x-coordinate of the first point.
- x2
- x-coordinate of the second point.
- y1
- y-coordinate of the first point (should be between 0 and 1).
- y2
- y-coordinate of the second point (should be between 0 and 1).
-
- Returns
- -------
- a
- Coefficient 'a' for the tanh function.
- b
- Coefficient 'b' for the tanh function.
-
- Notes
- -----
- The values of y1 and y2 should lie between 0 and 1, inclusive.
- """
- a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1)
- b = x1 - (math.atanh(2 * y1 - 1) / a)
- return a, b
-
-
-def scaled_tanh(
- x: float,
- a: float,
- b: float,
- y_min: float = 0.0,
- y_max: float = 100.0,
-) -> float:
- """Apply a scaled and shifted tanh function to a given input.
-
- This function represents a transformation of the tanh function that scales and shifts
- the output to lie between y_min and y_max. For values of 'x' close to 'x1' and 'x2'
- (used to calculate 'a' and 'b'), the output of this function will be close to 'y_min'
- and 'y_max', respectively.
-
- The equation of the function is as follows:
- y = y_min + (y_max - y_min) * 0.5 * (tanh(a * (x - b)) + 1)
-
- Parameters
- ----------
- x
- The input to the function.
- a
- The scale factor for the tanh function, found using 'find_a_b' function.
- b
- The shift factor for the tanh function, found using 'find_a_b' function.
- y_min
- The minimum value of the output range. Defaults to 0.
- y_max
- The maximum value of the output range. Defaults to 100.
-
- Returns
- -------
- float: The output of the function, which lies in the range [y_min, y_max].
- """
- return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1)
-
-
-def lerp_color_hsv(
- rgb1: tuple[float, float, float],
- rgb2: tuple[float, float, float],
- t: float,
-) -> tuple[int, int, int]:
- """Linearly interpolate between two RGB colors in HSV color space."""
- t = abs(t)
- assert 0 <= t <= 1
-
- # Convert RGB to HSV
- hsv1 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb1])
- hsv2 = colorsys.rgb_to_hsv(*[x / 255.0 for x in rgb2])
-
- # Linear interpolation in HSV space
- hsv = (
- hsv1[0] + t * (hsv2[0] - hsv1[0]),
- hsv1[1] + t * (hsv2[1] - hsv1[1]),
- hsv1[2] + t * (hsv2[2] - hsv1[2]),
- )
-
- # Convert back to RGB
- rgb = tuple(int(round(x * 255)) for x in colorsys.hsv_to_rgb(*hsv))
- assert all(0 <= x <= 255 for x in rgb), f"Invalid RGB color: {rgb}"
- return cast(tuple[int, int, int], rgb)
-
-
-def lerp(x, x1, x2, y1, y2):
- """Linearly interpolate between two values."""
- return y1 + (x - x1) * (y2 - y1) / (x2 - x1)
-
-
def int_to_base36(num: int) -> str:
"""Convert an integer to its base-36 representation using numbers and uppercase letters.
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 07eb0a97..35b1d7e5 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -2,12 +2,10 @@
from __future__ import annotations
import asyncio
-import bisect
import datetime
import logging
-import math
+import zoneinfo
from copy import deepcopy
-from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Any, Literal
@@ -60,8 +58,6 @@ from homeassistant.const import (
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
- SUN_EVENT_SUNRISE,
- SUN_EVENT_SUNSET,
)
from homeassistant.core import (
CALLBACK_TYPE,
@@ -83,9 +79,7 @@ from homeassistant.helpers.template import area_entities
from homeassistant.loader import bind_hass
from homeassistant.util import slugify
from homeassistant.util.color import (
- color_RGB_to_xy,
color_temperature_to_rgb,
- color_xy_to_hs,
color_xy_to_RGB,
)
@@ -96,6 +90,7 @@ from .adaptation_utils import (
ServiceData,
prepare_adaptation_data,
)
+from .color_and_brightness import SunLightSettings
from .const import (
ADAPT_BRIGHTNESS_SWITCH,
ADAPT_COLOR_SWITCH,
@@ -153,8 +148,6 @@ from .const import (
SERVICE_SET_MANUAL_CONTROL,
SET_MANUAL_CONTROL_SCHEMA,
SLEEP_MODE_SWITCH,
- SUN_EVENT_MIDNIGHT,
- SUN_EVENT_NOON,
TURNING_OFF_DELAY,
VALIDATION_TUPLES,
apply_service_schema,
@@ -164,19 +157,14 @@ from .hass_utils import setup_service_call_interceptor
from .helpers import (
clamp,
color_difference_redmean,
- find_a_b,
int_to_base36,
- lerp,
- lerp_color_hsv,
remove_vowels,
- scaled_tanh,
short_hash,
)
if TYPE_CHECKING:
from collections.abc import Callable, Coroutine, Iterable
- import astral
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -187,8 +175,6 @@ _SUPPORT_OPTS = {
"transition": SUPPORT_TRANSITION,
}
-_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT)
-_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))}
_LOGGER = logging.getLogger(__name__)
@@ -923,7 +909,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
brightness_mode=data[CONF_BRIGHTNESS_MODE],
brightness_mode_time_dark=data[CONF_BRIGHTNESS_MODE_TIME_DARK],
brightness_mode_time_light=data[CONF_BRIGHTNESS_MODE_TIME_LIGHT],
- transition=data[CONF_TRANSITION],
+ timezone=zoneinfo.ZoneInfo(self.hass.config.time_zone),
)
_LOGGER.debug(
"%s: Set switch settings for lights '%s'. now using data: '%s'",
@@ -1595,300 +1581,6 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
self._state = False
-@dataclass(frozen=True)
-class SunLightSettings:
- """Track the state of the sun and associated light settings."""
-
- name: str
- astral_location: astral.Location
- adapt_until_sleep: bool
- max_brightness: int
- max_color_temp: int
- min_brightness: int
- min_color_temp: int
- sleep_brightness: int
- sleep_rgb_or_color_temp: Literal["color_temp", "rgb_color"]
- sleep_color_temp: int
- sleep_rgb_color: tuple[int, int, int]
- sunrise_time: datetime.time | None
- sunrise_offset: datetime.timedelta | None
- min_sunrise_time: datetime.time | None
- max_sunrise_time: datetime.time | None
- sunset_time: datetime.time | None
- sunset_offset: datetime.timedelta | None
- min_sunset_time: datetime.time | None
- max_sunset_time: datetime.time | None
- brightness_mode: Literal["default", "linear", "tanh"]
- brightness_mode_time_dark: datetime.timedelta | None
- brightness_mode_time_light: datetime.timedelta | None
- transition: int
-
- def sunrise(self, date: datetime.datetime) -> datetime.datetime:
- """Return the (adjusted) sunrise time for the given date."""
- sunrise = (
- self.astral_location.sunrise(date, local=False)
- if self.sunrise_time is None
- else self._replace_time(date, "sunrise")
- ) + self.sunrise_offset
- if self.min_sunrise_time is not None:
- min_sunrise = self._replace_time(date, "min_sunrise")
- if min_sunrise > sunrise:
- sunrise = min_sunrise
- if self.max_sunrise_time is not None:
- max_sunrise = self._replace_time(date, "max_sunrise")
- if max_sunrise < sunrise:
- sunrise = max_sunrise
- return sunrise
-
- def sunset(self, date: datetime.datetime) -> datetime.datetime:
- """Return the (adjusted) sunset time for the given date."""
- sunset = (
- self.astral_location.sunset(date, local=False)
- if self.sunset_time is None
- else self._replace_time(date, "sunset")
- ) + self.sunset_offset
- if self.min_sunset_time is not None:
- min_sunset = self._replace_time(date, "min_sunset")
- if min_sunset > sunset:
- sunset = min_sunset
- if self.max_sunset_time is not None:
- max_sunset = self._replace_time(date, "max_sunset")
- if max_sunset < sunset:
- sunset = max_sunset
- return sunset
-
- def _replace_time(self, date: datetime.datetime, key: str) -> datetime.datetime:
- time = getattr(self, f"{key}_time")
- date_time = datetime.datetime.combine(date, time)
- return date_time.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(
- dt_util.UTC,
- )
-
- def get_sun_events(self, date: datetime.datetime) -> list[tuple[str, float]]:
- """Get the four sun event's timestamps at 'date'."""
-
- def calculate_noon_and_midnight(
- sunset: datetime.datetime,
- sunrise: datetime.datetime,
- ) -> tuple[datetime.datetime, datetime.datetime]:
- middle = abs(sunset - sunrise) / 2
- if sunset > sunrise:
- noon = sunrise + middle
- midnight = noon + timedelta(hours=12) * (1 if noon.hour < 12 else -1)
- else:
- midnight = sunset + middle
- noon = midnight + timedelta(hours=12) * (
- 1 if midnight.hour < 12 else -1
- )
- return noon, midnight
-
- location = self.astral_location
- sunrise = self.sunrise(date)
- sunset = self.sunset(date)
-
- if (
- self.sunrise_time is None
- and self.sunset_time is None
- and self.min_sunrise_time is None
- and self.max_sunrise_time is None
- and self.min_sunset_time is None
- and self.max_sunset_time is None
- ):
- solar_noon = location.noon(date, local=False)
- solar_midnight = location.midnight(date, local=False)
- else:
- solar_noon, solar_midnight = calculate_noon_and_midnight(sunset, sunrise)
-
- events = [
- (SUN_EVENT_SUNRISE, sunrise.timestamp()),
- (SUN_EVENT_SUNSET, sunset.timestamp()),
- (SUN_EVENT_NOON, solar_noon.timestamp()),
- (SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()),
- ]
- # Check whether order is correct
- events = sorted(events, key=lambda x: x[1])
- events_names, _ = zip(*events, strict=True)
- if events_names not in _ALLOWED_ORDERS:
- msg = (
- f"{self.name}: The sun events {events_names} are not in the expected"
- " order. The Adaptive Lighting integration will not work!"
- " This might happen if your sunrise/sunset offset is too large or"
- " your manually set sunrise/sunset time is past/before noon/midnight."
- )
- _LOGGER.error(msg)
- raise ValueError(msg)
-
- return events
-
- def relevant_events(self, now: datetime.datetime) -> list[tuple[str, float]]:
- """Get the previous and next sun event."""
- events = [
- event
- for days in [-1, 0, 1]
- for event in self.get_sun_events(now + timedelta(days=days))
- ]
- events = sorted(events, key=lambda x: x[1])
- i_now = bisect.bisect([ts for _, ts in events], now.timestamp())
- return events[i_now - 1 : i_now + 1]
-
- def calc_percent(self, transition: int) -> float:
- """Calculate the position of the sun in %."""
- now = dt_util.utcnow()
-
- target_time = now + timedelta(seconds=transition)
- target_ts = target_time.timestamp()
- today = self.relevant_events(target_time)
- (_, prev_ts), (next_event, next_ts) = today
- h, x = ( # pylint: disable=invalid-name
- (prev_ts, next_ts)
- if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
- else (next_ts, prev_ts)
- )
- k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1
- return (0 - k) * ((target_ts - h) / (h - x)) ** 2 + k
-
- def calc_brightness_pct(self, percent: float, is_sleep: bool) -> float:
- """Calculate the brightness in %."""
- if is_sleep:
- return self.sleep_brightness
- assert self.brightness_mode in ("default", "linear", "tanh")
-
- if self.brightness_mode == "default":
- if percent > 0:
- return self.max_brightness
- delta_brightness = self.max_brightness - self.min_brightness
- percent = 1 + percent
- return (delta_brightness * percent) + self.min_brightness
-
- now = dt_util.utcnow()
- (prev_event, prev_ts), (next_event, next_ts) = self.relevant_events(now)
-
- # at ts_event - dt_start, brightness == start_brightness
- # at ts_event + dt_end, brightness == end_brightness
- dark = (self.brightness_mode_time_dark or timedelta()).total_seconds()
- light = (self.brightness_mode_time_light or timedelta()).total_seconds()
- # Handle sunrise
- if prev_event == SUN_EVENT_SUNRISE or next_event == SUN_EVENT_SUNRISE:
- ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts
- if self.brightness_mode == "linear":
- brightness = lerp(
- now.timestamp(),
- x1=ts_event - dark,
- x2=ts_event + light,
- y1=self.min_brightness,
- y2=self.max_brightness,
- )
- else:
- assert self.brightness_mode == "tanh"
- a, b = find_a_b(
- x1=-dark,
- x2=+light,
- y1=0.05, # be at 5% of range at x1
- y2=0.95, # be at 95% of range at x2
- )
- brightness = scaled_tanh(
- now.timestamp() - ts_event,
- a=a,
- b=b,
- y_min=self.min_brightness,
- y_max=self.max_brightness,
- )
- # Handle sunset
- elif prev_event == SUN_EVENT_SUNSET or next_event == SUN_EVENT_SUNSET:
- ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts
- if self.brightness_mode == "linear":
- brightness = lerp(
- now.timestamp(),
- x1=ts_event - light,
- x2=ts_event + dark,
- y1=self.max_brightness,
- y2=self.min_brightness,
- )
- else:
- assert self.brightness_mode == "tanh"
- a, b = find_a_b(
- x1=-light, # shifted timestamp for the start of sunset
- x2=+dark, # shifted timestamp for the end of sunset
- y1=0.95, # be at 95% of range at the start of sunset
- y2=0.05, # be at 5% of range at the end of sunset
- )
- brightness = scaled_tanh(
- now.timestamp() - ts_event,
- a=a,
- b=b,
- y_min=self.min_brightness,
- y_max=self.max_brightness,
- )
- return clamp(brightness, self.min_brightness, self.max_brightness)
-
- def calc_color_temp_kelvin(self, percent: float) -> int:
- """Calculate the color temperature in Kelvin."""
- if percent > 0:
- delta = self.max_color_temp - self.min_color_temp
- ct = (delta * percent) + self.min_color_temp
- return 5 * round(ct / 5) # round to nearest 5
- if percent == 0 or not self.adapt_until_sleep:
- return self.min_color_temp
- if self.adapt_until_sleep and percent < 0:
- delta = abs(self.min_color_temp - self.sleep_color_temp)
- ct = (delta * abs(1 + percent)) + self.sleep_color_temp
- return 5 * round(ct / 5) # round to nearest 5
- msg = "Should not happen"
- raise ValueError(msg)
-
- def get_settings(
- self,
- is_sleep,
- transition,
- ) -> dict[str, float | int | tuple[float, float] | tuple[float, float, float]]:
- """Get all light settings.
-
- Calculating all values takes <0.5ms.
- """
- percent = (
- self.calc_percent(transition)
- if transition is not None
- else self.calc_percent(0)
- )
- rgb_color: tuple[float, float, float]
- # Variable `force_rgb_color` is needed for RGB color after sunset (if enabled)
- force_rgb_color = False
- brightness_pct = self.calc_brightness_pct(percent, is_sleep)
- if is_sleep:
- color_temp_kelvin = self.sleep_color_temp
- rgb_color = self.sleep_rgb_color
- elif (
- self.sleep_rgb_or_color_temp == "rgb_color"
- and self.adapt_until_sleep
- and percent < 0
- ):
- # Feature requested in
- # https://github.com/basnijholt/adaptive-lighting/issues/624
- # This will result in a perceptible jump in color at sunset and sunrise
- # because the `color_temperature_to_rgb` function is not 100% accurate.
- min_color_rgb = color_temperature_to_rgb(self.min_color_temp)
- rgb_color = lerp_color_hsv(min_color_rgb, self.sleep_rgb_color, percent)
- color_temp_kelvin = self.calc_color_temp_kelvin(percent)
- force_rgb_color = True
- else:
- color_temp_kelvin = self.calc_color_temp_kelvin(percent)
- rgb_color = color_temperature_to_rgb(color_temp_kelvin)
- # backwards compatibility for versions < 1.3.1 - see #403
- color_temp_mired: float = math.floor(1000000 / color_temp_kelvin)
- xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color)
- hs_color: tuple[float, float] = color_xy_to_hs(*xy_color)
- return {
- "brightness_pct": brightness_pct,
- "color_temp_kelvin": color_temp_kelvin,
- "color_temp_mired": color_temp_mired,
- "rgb_color": rgb_color,
- "xy_color": xy_color,
- "hs_color": hs_color,
- "sun_position": percent,
- "force_rgb_color": force_rgb_color,
- }
-
-
class AdaptiveLightingManager:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
diff --git a/tests/test_color_and_brightness.py b/tests/test_color_and_brightness.py
new file mode 100644
index 00000000..a199f939
--- /dev/null
+++ b/tests/test_color_and_brightness.py
@@ -0,0 +1,209 @@
+import pytest
+from custom_components.adaptive_lighting.color_and_brightness import (
+ SunEvents,
+ SUN_EVENT_SUNRISE,
+ SUN_EVENT_NOON,
+)
+import datetime as dt
+from astral import LocationInfo
+from astral.location import Location
+import zoneinfo
+
+# Create a mock astral_location object
+location = Location(LocationInfo())
+
+LAT_LONG_TZS = [
+ (52.379189, 4.899431, "Europe/Amsterdam"),
+ (32.87336, -117.22743, "US/Pacific"),
+ (60, 50, "GMT"),
+ (60, 50, "UTC"),
+]
+
+
+@pytest.fixture(params=LAT_LONG_TZS)
+def tzinfo_and_location(request):
+ lat, long, timezone = request.param
+ tzinfo = zoneinfo.ZoneInfo(timezone)
+ location = Location(
+ LocationInfo(
+ name="name",
+ region="region",
+ timezone=timezone,
+ latitude=lat,
+ longitude=long,
+ )
+ )
+ return tzinfo, location
+
+
+def test_replace_time(tzinfo_and_location):
+ tzinfo, location = tzinfo_and_location
+ sun_events = SunEvents(
+ name="test",
+ astral_location=location,
+ sunrise_time=None,
+ min_sunrise_time=None,
+ max_sunrise_time=None,
+ sunset_time=None,
+ min_sunset_time=None,
+ max_sunset_time=None,
+ timezone=tzinfo,
+ )
+
+ new_time = dt.time(5, 30)
+ datetime = dt.datetime(2022, 1, 1)
+ replaced_time_utc = sun_events._replace_time(datetime.date(), new_time)
+ assert replaced_time_utc.astimezone(tzinfo).time() == new_time
+
+
+def test_sunrise_without_offset(tzinfo_and_location):
+ tzinfo, location = tzinfo_and_location
+
+ sun_events = SunEvents(
+ name="test",
+ astral_location=location,
+ sunrise_time=None,
+ min_sunrise_time=None,
+ max_sunrise_time=None,
+ sunset_time=None,
+ min_sunset_time=None,
+ max_sunset_time=None,
+ timezone=tzinfo,
+ )
+ date = dt.datetime(2022, 1, 1).date()
+ result = sun_events.sunrise(date)
+ assert result == location.sunrise(date)
+
+
+def test_sun_position_no_fixed_sunset_and_sunrise(tzinfo_and_location):
+ tzinfo, location = tzinfo_and_location
+ sun_events = SunEvents(
+ name="test",
+ astral_location=location,
+ sunrise_time=None,
+ min_sunrise_time=None,
+ max_sunrise_time=None,
+ sunset_time=None,
+ min_sunset_time=None,
+ max_sunset_time=None,
+ timezone=tzinfo,
+ )
+ date = dt.datetime(2022, 1, 1).date()
+ sunset = location.sunset(date)
+ position = sun_events.sun_position(sunset)
+ assert position == 0
+ sunrise = location.sunrise(date)
+ position = sun_events.sun_position(sunrise)
+ assert position == 0
+ noon = location.noon(date)
+ position = sun_events.sun_position(noon)
+ assert position == 1
+ midnight = location.midnight(date)
+ position = sun_events.sun_position(midnight)
+ assert position == -1
+
+
+def test_sun_position_fixed_sunset_and_sunrise(tzinfo_and_location):
+ tzinfo, location = tzinfo_and_location
+ sun_events = SunEvents(
+ name="test",
+ astral_location=location,
+ sunrise_time=dt.time(6, 0),
+ min_sunrise_time=None,
+ max_sunrise_time=None,
+ sunset_time=dt.time(18, 0),
+ min_sunset_time=None,
+ max_sunset_time=None,
+ timezone=tzinfo,
+ )
+ date = dt.datetime(2022, 1, 1).date()
+ sunset = sun_events.sunset(date)
+ position = sun_events.sun_position(sunset)
+ assert position == 0
+ sunrise = sun_events.sunrise(date)
+ position = sun_events.sun_position(sunrise)
+ assert position == 0
+ noon, midnight = sun_events.noon_and_midnight(date)
+ position = sun_events.sun_position(noon)
+ assert position == 1
+ position = sun_events.sun_position(midnight)
+ assert position == -1
+
+
+def test_noon_and_midnight(tzinfo_and_location):
+ tzinfo, location = tzinfo_and_location
+ sun_events = SunEvents(
+ name="test",
+ astral_location=location,
+ sunrise_time=None,
+ min_sunrise_time=None,
+ max_sunrise_time=None,
+ sunset_time=None,
+ min_sunset_time=None,
+ max_sunset_time=None,
+ timezone=tzinfo,
+ )
+ date = dt.datetime(2022, 1, 1)
+ noon, midnight = sun_events.noon_and_midnight(date)
+ assert noon == location.noon(date)
+ assert midnight == location.midnight(date)
+
+
+def test_sun_events(tzinfo_and_location):
+ tzinfo, location = tzinfo_and_location
+ sun_events = SunEvents(
+ name="test",
+ astral_location=location,
+ sunrise_time=None,
+ min_sunrise_time=None,
+ max_sunrise_time=None,
+ sunset_time=None,
+ min_sunset_time=None,
+ max_sunset_time=None,
+ timezone=tzinfo,
+ )
+
+ date = dt.datetime(2022, 1, 1)
+ events = sun_events.sun_events(date)
+ assert len(events) == 4
+ assert (SUN_EVENT_SUNRISE, location.sunrise(date).timestamp()) in events
+
+
+def test_prev_and_next_events(tzinfo_and_location):
+ tzinfo, location = tzinfo_and_location
+ sun_events = SunEvents(
+ name="test",
+ astral_location=location,
+ sunrise_time=None,
+ min_sunrise_time=None,
+ max_sunrise_time=None,
+ sunset_time=None,
+ min_sunset_time=None,
+ max_sunset_time=None,
+ timezone=tzinfo,
+ )
+ datetime = dt.datetime(2022, 1, 1, 10, 0)
+ after_sunrise = sun_events.sunrise(datetime.date()) + dt.timedelta(hours=1)
+ prev_event, next_event = sun_events.prev_and_next_events(after_sunrise)
+ assert prev_event[0] == SUN_EVENT_SUNRISE
+ assert next_event[0] == SUN_EVENT_NOON
+
+
+def test_closest_event(tzinfo_and_location):
+ tzinfo, location = tzinfo_and_location
+ sun_events = SunEvents(
+ name="test",
+ astral_location=location,
+ sunrise_time=None,
+ min_sunrise_time=None,
+ max_sunrise_time=None,
+ sunset_time=None,
+ min_sunset_time=None,
+ max_sunset_time=None,
+ timezone=tzinfo,
+ )
+ datetime = dt.datetime(2022, 1, 1, 6, 0)
+ sunrise = sun_events.sunrise(datetime.date())
+ event_name, ts = sun_events.closest_event(sunrise)
+ assert event_name == SUN_EVENT_SUNRISE
+ assert ts == location.sunrise(sunrise.date()).timestamp()
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 79dc9b20..6ffd1a21 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -100,8 +100,8 @@ from custom_components.adaptive_lighting.switch import (
AdaptiveLightingManager,
is_our_context,
is_our_context_id,
- lerp_color_hsv,
)
+from custom_components.adaptive_lighting.color_and_brightness import lerp_color_hsv
_LOGGER = logging.getLogger(__name__)
@@ -394,6 +394,7 @@ async def test_adaptive_lighting_time_zones_and_sun_settings(
min_color_temp = switch._sun_light_settings.min_color_temp
sunset = SUNSET.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC)
+
before_sunset = sunset - datetime.timedelta(hours=1)
after_sunset = sunset + datetime.timedelta(hours=1)
sunrise = SUNRISE.replace(tzinfo=dt_util.DEFAULT_TIME_ZONE).astimezone(dt_util.UTC)
@@ -401,7 +402,10 @@ async def test_adaptive_lighting_time_zones_and_sun_settings(
after_sunrise = sunrise + datetime.timedelta(hours=1)
async def patch_time_and_update(time):
- with patch("homeassistant.util.dt.utcnow", return_value=time):
+ with patch(
+ "custom_components.adaptive_lighting.color_and_brightness.utcnow",
+ return_value=time,
+ ):
await switch._update_attrs_and_maybe_adapt_lights(context=context)
await hass.async_block_till_done()
@@ -490,7 +494,10 @@ async def test_light_settings(hass):
context = switch.create_context("test") # needs to be passed to update method
async def patch_time_and_get_updated_states(time):
- with patch("homeassistant.util.dt.utcnow", return_value=time):
+ with patch(
+ "custom_components.adaptive_lighting.color_and_brightness.utcnow",
+ return_value=time,
+ ):
await switch._update_attrs_and_maybe_adapt_lights(
context=context, transition=0, force=True
)
@@ -1818,7 +1825,10 @@ async def test_adapt_until_sleep_and_rgb_colors(hass):
after_sunrise = sunrise + datetime.timedelta(hours=1)
async def patch_time_and_update(time):
- with patch("homeassistant.util.dt.utcnow", return_value=time):
+ with patch(
+ "custom_components.adaptive_lighting.color_and_brightness.utcnow",
+ return_value=time,
+ ):
await switch._update_attrs_and_maybe_adapt_lights(context=context)
await hass.async_block_till_done()
@@ -2072,7 +2082,10 @@ async def test_brightness_mode(hass, brightness_mode, dark, light):
return abs(a - b) < 0.01
async def patch_time_and_update(time):
- with patch("homeassistant.util.dt.utcnow", return_value=time):
+ with patch(
+ "custom_components.adaptive_lighting.color_and_brightness.utcnow",
+ return_value=time,
+ ):
await switch._update_attrs_and_maybe_adapt_lights(context=context)
await hass.async_block_till_done()
diff --git a/webapp/app.py b/webapp/app.py
index 208ea242..91a33d0c 100644
--- a/webapp/app.py
+++ b/webapp/app.py
@@ -1,206 +1,181 @@
"""Simple web app to visualize brightness over time."""
-import math
-
import matplotlib.pyplot as plt
import numpy as np
from shiny import App, render, ui
+from pathlib import Path
+from contextlib import suppress
+import datetime as dt
+from astral import LocationInfo
+from astral.location import Location
-def lerp(x, x1, x2, y1, y2):
- """Linearly interpolate between two values."""
- return y1 + (x - x1) * (y2 - y1) / (x2 - x1)
-
-
-def clamp(value: float, minimum: float, maximum: float) -> float:
- """Clamp value between minimum and maximum."""
- return max(minimum, min(value, maximum))
-
-
-def find_a_b(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]:
- a = (math.atanh(2 * y2 - 1) - math.atanh(2 * y1 - 1)) / (x2 - x1)
- b = x1 - (math.atanh(2 * y1 - 1) / a)
- return a, b
-
-
-def scaled_tanh(
- x: float,
- a: float,
- b: float,
- y_min: float = 0.0,
- y_max: float = 1.0,
-) -> float:
- """Apply a scaled and shifted tanh function to a given input."""
- return y_min + (y_max - y_min) * 0.5 * (math.tanh(a * (x - b)) + 1)
-
-
-def is_closer_to_sunrise_than_sunset(time, sunrise_time, sunset_time):
- """Return True if the time is closer to sunrise than sunset."""
- return abs(time - sunrise_time) < abs(time - sunset_time)
-
-
-def brightness_linear(
- time,
- sunrise_time,
- sunset_time,
- time_light,
- time_dark,
- max_brightness,
- min_brightness,
-):
- """Calculate the brightness for the 'linear' mode."""
- closer_to_sunrise = is_closer_to_sunrise_than_sunset(
- time,
- sunrise_time,
- sunset_time,
+def date_range(tzinfo):
+ start_of_day = dt.datetime.now(tzinfo).replace(
+ hour=0, minute=0, second=0, microsecond=0
)
- if closer_to_sunrise:
- brightness = lerp(
- time,
- x1=sunrise_time - time_dark,
- x2=sunrise_time + time_light,
- y1=min_brightness,
- y2=max_brightness,
- )
- else:
- brightness = lerp(
- time,
- x1=sunset_time - time_light,
- x2=sunset_time + time_dark,
- y1=max_brightness,
- y2=min_brightness,
- )
- return clamp(brightness, min_brightness, max_brightness)
+ # one second before the next day
+ end_of_day = start_of_day + dt.timedelta(days=1) - dt.timedelta(seconds=1)
+ hours_range = [start_of_day]
+ while hours_range[-1] < end_of_day:
+ hours_range.append(hours_range[-1] + dt.timedelta(minutes=5))
+ return hours_range[:-1]
-def brightness_tanh(
- time,
- sunrise_time,
- sunset_time,
- time_light,
- time_dark,
- max_brightness,
- min_brightness,
-):
- """Calculate the brightness for the 'tanh' mode."""
- closer_to_sunrise = is_closer_to_sunrise_than_sunset(
- time,
- sunrise_time,
- sunset_time,
- )
- if closer_to_sunrise:
- a, b = find_a_b(
- x1=-time_dark,
- x2=time_light,
- y1=0.05, # be at 5% of range at x1
- y2=0.95, # be at 95% of range at x2
+def copy_color_and_brightness_module():
+ with suppress(Exception):
+ webapp_folder = Path(__file__).parent.absolute()
+ module = (
+ webapp_folder.parent
+ / "custom_components"
+ / "adaptive_lighting"
+ / "color_and_brightness.py"
)
- brightness = scaled_tanh(
- time - sunrise_time,
- a=a,
- b=b,
- y_min=min_brightness,
- y_max=max_brightness,
- )
- else:
- a, b = find_a_b(
- x1=-time_light, # shifted timestamp for the start of sunset
- x2=time_dark, # shifted timestamp for the end of sunset
- y1=0.95, # be at 95% of range at the start of sunset
- y2=0.05, # be at 5% of range at the end of sunset
- )
- brightness = scaled_tanh(
- time - sunset_time,
- a=a,
- b=b,
- y_min=min_brightness,
- y_max=max_brightness,
- )
- return clamp(brightness, min_brightness, max_brightness)
+ new_module = webapp_folder / module.name
+ with module.open() as f:
+ lines = [
+ line.replace("homeassistant.util.color", "homeassistant_util_color")
+ for line in f.readlines()
+ ]
+ with new_module.open("r") as f:
+ existing_lines = f.readlines()
+ if existing_lines != lines:
+ with new_module.open("w") as f:
+ f.writelines(lines)
-def plot_brightness(
- min_brightness,
- max_brightness,
- brightness_mode_time_dark,
- brightness_mode_time_light,
- sunrise_time=6, # 6 AM
- sunset_time=18, # 6 PM
-):
+copy_color_and_brightness_module()
+
+from color_and_brightness import SunLightSettings
+
+
+def plot_brightness(kw, sleep_mode: bool):
# Define the time range for our simulation
- time_range = np.linspace(0, 24, 1000) # From 0 to 24 hours
-
- # Calculate the brightness for each time in the time range for both modes
+ sun_linear = SunLightSettings(**kw, brightness_mode="linear")
+ sun_tanh = SunLightSettings(**kw, brightness_mode="tanh")
+ sun = SunLightSettings(**kw, brightness_mode="default")
+ # Calculate the brightness for each time in the time range for all modes
+ dt_range = date_range(sun.timezone)
+ time_range = [time_to_float(dt) for dt in dt_range]
brightness_linear_values = [
- brightness_linear(
- time,
- sunrise_time,
- sunset_time,
- brightness_mode_time_light,
- brightness_mode_time_dark,
- max_brightness,
- min_brightness,
- )
- for time in time_range
+ sun_linear.brightness_pct(dt, sleep_mode) for dt in dt_range
]
brightness_tanh_values = [
- brightness_tanh(
- time,
- sunrise_time,
- sunset_time,
- brightness_mode_time_light,
- brightness_mode_time_dark,
- max_brightness,
- min_brightness,
- )
- for time in time_range
+ sun_tanh.brightness_pct(dt, sleep_mode) for dt in dt_range
]
+ brightness_default_values = [sun.brightness_pct(dt, sleep_mode) for dt in dt_range]
# Plot the brightness over time for both modes
- plt.figure(figsize=(10, 6))
- plt.plot(time_range, brightness_linear_values, label="Linear Mode")
- plt.plot(time_range, brightness_tanh_values, label="Tanh Mode")
- plt.vlines(sunrise_time, 0, 1, color="C2", label="Sunrise", linestyles="dashed")
- plt.vlines(sunset_time, 0, 1, color="C3", label="Sunset", linestyles="dashed")
- plt.xlim(0, 24)
- plt.xticks(np.arange(0, 25, 1))
- yticks = np.arange(0, 1.05, 0.05)
- ytick_labels = [f"{100*label:.0f}%" for label in yticks]
- plt.yticks(yticks, ytick_labels)
- plt.xlabel("Time (hours)")
- plt.ylabel("Brightness")
- plt.title("Brightness over Time for Different Modes")
+ fig, ax = plt.subplots(figsize=(10, 6))
+ ax.plot(time_range, brightness_linear_values, label="Linear Mode")
+ ax.plot(time_range, brightness_tanh_values, label="Tanh Mode")
+ ax.plot(time_range, brightness_default_values, label="Default Mode")
+ sunrise_time = sun.sun.sunrise(dt.date.today())
+ sunset_time = sun.sun.sunset(dt.date.today())
+ ax.vlines(
+ time_to_float(sunrise_time),
+ 0,
+ 100,
+ color="C2",
+ label="Sunrise",
+ linestyles="dashed",
+ )
+ ax.vlines(
+ time_to_float(sunset_time),
+ 0,
+ 100,
+ color="C3",
+ label="Sunset",
+ linestyles="dashed",
+ )
+ ax.set_xlim(0, 24)
+ ax.set_xticks(np.arange(0, 25, 1))
+ yticks = np.arange(0, 105, 5)
+ ytick_labels = [f"{label:.0f}%" for label in yticks]
+ ax.set_yticks(yticks, ytick_labels)
+ ax.set_xlabel("Time (hours)")
+ ax.set_ylabel("Brightness")
+ ax.set_title("Brightness over Time for Different Modes")
# Add text box
textstr = "\n".join(
(
- f"Sunrise Time = {sunrise_time}:00:00",
- f"Sunset Time = {sunset_time}:00:00",
- f"Max Brightness = {max_brightness*100:.0f}%",
- f"Min Brightness = {min_brightness*100:.0f}%",
- f"Time Light = {brightness_mode_time_light:.1f} hours",
- f"Time Dark = {brightness_mode_time_dark:.1f} hours",
+ f"Sunrise Time = {sunrise_time.time()}",
+ f"Sunset Time = {sunset_time.time()}",
+ f"Max Brightness = {sun.max_brightness:.0f}%",
+ f"Min Brightness = {sun.min_brightness:.0f}%",
+ f"Time Light = {sun.brightness_mode_time_light}",
+ f"Time Dark = {sun.brightness_mode_time_dark}",
),
)
- # these are matplotlib.patch.Patch properties
- props = {"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5}
+ ax.legend()
+ ax.grid(True)
- plt.legend()
- plt.grid(True)
-
- # place a text box in upper left in axes coords
- plt.gca().text(
+ ax.text(
0.4,
0.55,
textstr,
- transform=plt.gca().transAxes,
+ transform=ax.transAxes,
fontsize=10,
verticalalignment="center",
- bbox=props,
+ bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
)
- return plt.gcf()
+ return fig
+
+
+def plot_color_temp(kw, sleep_mode: bool):
+ sun = SunLightSettings(**kw, brightness_mode="default")
+ dt_range = date_range(tzinfo=sun.timezone)
+ time_range = [time_to_float(dt) for dt in dt_range]
+ settings = [sun.brightness_and_color(dt, sleep_mode) for dt in dt_range]
+ color_temp_values = (
+ np.array([(*setting["rgb_color"], 255) for setting in settings]) / 255
+ )
+ color_temp_values = color_temp_values.reshape(-1, 1, 4)
+ sun_position = [setting["sun_position"] for setting in settings]
+ fig, ax = plt.subplots(figsize=(10, 6))
+
+ # Display as a horizontal bar
+ ax.imshow(
+ np.rot90(color_temp_values)[:, ::1],
+ aspect="auto",
+ extent=[0, 24, -1, 1],
+ origin="upper",
+ )
+ # Plot a curve on top of the imshow
+ ax.plot(time_range, sun_position, color="k", label="Sun Position")
+
+ sunrise_time = sun.sun.sunrise(dt.date.today())
+ sunset_time = sun.sun.sunset(dt.date.today())
+ ax.vlines(
+ time_to_float(sunrise_time),
+ -1,
+ 1,
+ color="C2",
+ label="Sunrise",
+ linestyles="dashed",
+ )
+ ax.vlines(
+ time_to_float(sunset_time),
+ -1,
+ 1,
+ color="C3",
+ label="Sunset",
+ linestyles="dashed",
+ )
+
+ ax.set_xlim(0, 24)
+ ax.set_xticks(np.arange(0, 25, 1))
+ yticks = np.arange(-1, 1.1, 0.1)
+ ax.set_yticks(yticks, [f"{label*100:.0f}%" for label in yticks])
+ ax.set_xlabel("Time (hours)")
+ ax.legend()
+ ax.set_ylabel("Sun position (%)")
+ ax.set_title("RGB Color Intensity over Time")
+
+ return fig
SEC_PER_HR = 60 * 60
@@ -225,20 +200,34 @@ app_ui = ui.page_fluid(
ui.panel_title("🌞 Adaptive Lighting Simulator WebApp 🌛"),
ui.layout_sidebar(
ui.panel_sidebar(
- ui.input_slider("min_brightness", "min_brightness", 0, 100, 30, post="%"),
- ui.input_slider("max_brightness", "max_brightness", 0, 100, 100, post="%"),
+ ui.input_switch("adapt_until_sleep", "adapt_until_sleep", False),
+ ui.input_switch("sleep_mode", "sleep_mode", False),
+ ui.input_slider("min_brightness", "min_brightness", 1, 100, 30, post="%"),
+ ui.input_slider("max_brightness", "max_brightness", 1, 100, 100, post="%"),
+ ui.input_numeric("min_color_temp", "min_color_temp", 2000),
+ ui.input_numeric("max_color_temp", "max_color_temp", 6666),
+ ui.input_slider(
+ "sleep_brightness", "sleep_brightness", 1, 100, 1, post="%"
+ ),
+ ui.input_radio_buttons(
+ "sleep_rgb_or_color_temp",
+ "sleep_rgb_or_color_temp",
+ ["rgb_color", "color_temp"],
+ ),
+ ui.input_numeric("sleep_color_temp", "sleep_color_temp", 2000),
+ ui.input_text("sleep_rgb_color", "sleep_rgb_color", "255,0,0"),
ui.input_slider(
- "dark_time",
"brightness_mode_time_dark",
- 0,
+ "brightness_mode_time_dark",
+ 1,
5 * SEC_PER_HR,
3 * SEC_PER_HR,
post=" sec",
),
ui.input_slider(
- "light_time",
"brightness_mode_time_light",
- 0,
+ "brightness_mode_time_light",
+ 1,
5 * SEC_PER_HR,
0.5 * SEC_PER_HR,
post=" sec",
@@ -262,23 +251,68 @@ app_ui = ui.page_fluid(
post=" hr",
),
),
- ui.panel_main(ui.markdown(desc), ui.output_plot(id="brightness_plot")),
+ ui.panel_main(
+ ui.markdown(desc),
+ ui.output_plot(id="brightness_plot"),
+ ui.output_plot(id="color_temp_plot"),
+ ),
),
)
+def float_to_time(value: float) -> dt.time:
+ hours = int(value)
+ minutes = int((value - hours) * 60)
+ time = dt.time(hours, minutes)
+ return time
+
+
+def time_to_float(time: dt.time | dt.datetime) -> float:
+ return time.hour + time.minute / 60
+
+
+def _kw(input):
+ location = Location(LocationInfo(timezone=dt.timezone.utc))
+ return dict(
+ name="Adaptive Lighting Simulator",
+ adapt_until_sleep=input.adapt_until_sleep(),
+ max_brightness=input.max_brightness(),
+ min_brightness=input.min_brightness(),
+ min_color_temp=input.min_color_temp(),
+ max_color_temp=input.max_color_temp(),
+ sleep_brightness=input.sleep_brightness(),
+ sleep_rgb_or_color_temp=input.sleep_rgb_or_color_temp(),
+ sleep_color_temp=input.sleep_color_temp(),
+ sleep_rgb_color=[int(x) for x in input.sleep_rgb_color().split(",")],
+ sunrise_time=float_to_time(input.sunrise_time()),
+ sunset_time=float_to_time(input.sunset_time()),
+ brightness_mode_time_dark=dt.timedelta(
+ seconds=input.brightness_mode_time_dark()
+ ),
+ brightness_mode_time_light=dt.timedelta(
+ seconds=input.brightness_mode_time_light()
+ ),
+ sunrise_offset=dt.timedelta(0),
+ sunset_offset=dt.timedelta(0),
+ min_sunrise_time=None,
+ max_sunrise_time=None,
+ min_sunset_time=None,
+ max_sunset_time=None,
+ astral_location=location,
+ timezone=location.timezone,
+ )
+
+
def server(input, output, session):
@output
@render.plot
def brightness_plot():
- return plot_brightness(
- min_brightness=input.min_brightness() / 100,
- max_brightness=input.max_brightness() / 100,
- brightness_mode_time_dark=input.dark_time() / SEC_PER_HR,
- brightness_mode_time_light=input.light_time() / SEC_PER_HR,
- sunrise_time=input.sunrise_time(),
- sunset_time=input.sunset_time(),
- )
+ return plot_brightness(_kw(input), sleep_mode=input.sleep_mode())
+
+ @output
+ @render.plot
+ def color_temp_plot():
+ return plot_color_temp(_kw(input), sleep_mode=input.sleep_mode())
app = App(app_ui, server)
diff --git a/webapp/homeassistant_util_color.py b/webapp/homeassistant_util_color.py
new file mode 100644
index 00000000..33df5cf3
--- /dev/null
+++ b/webapp/homeassistant_util_color.py
@@ -0,0 +1,773 @@
+"""Color util methods."""
+# Slightly modified from homeassistant.util.color at
+# https://github.com/home-assistant/core/blob/798fb3e31a6ba87358adc93a4c5b772b64451712/homeassistant/util/color.py#L14
+# to remove the dependency on homeassistant.util.color in sun.py
+from __future__ import annotations
+
+import colorsys
+import math
+from dataclasses import dataclass
+from typing import NamedTuple
+
+
+class RGBColor(NamedTuple):
+ """RGB hex values."""
+
+ r: int
+ g: int
+ b: int
+
+
+# Official CSS3 colors from w3.org:
+# https://www.w3.org/TR/2010/PR-css3-color-20101028/#html4
+# names do not have spaces in them so that we can compare against
+# requests more easily (by removing spaces from the requests as well).
+# This lets "dark seagreen" and "dark sea green" both match the same
+# color "darkseagreen".
+COLORS = {
+ "aliceblue": RGBColor(240, 248, 255),
+ "antiquewhite": RGBColor(250, 235, 215),
+ "aqua": RGBColor(0, 255, 255),
+ "aquamarine": RGBColor(127, 255, 212),
+ "azure": RGBColor(240, 255, 255),
+ "beige": RGBColor(245, 245, 220),
+ "bisque": RGBColor(255, 228, 196),
+ "black": RGBColor(0, 0, 0),
+ "blanchedalmond": RGBColor(255, 235, 205),
+ "blue": RGBColor(0, 0, 255),
+ "blueviolet": RGBColor(138, 43, 226),
+ "brown": RGBColor(165, 42, 42),
+ "burlywood": RGBColor(222, 184, 135),
+ "cadetblue": RGBColor(95, 158, 160),
+ "chartreuse": RGBColor(127, 255, 0),
+ "chocolate": RGBColor(210, 105, 30),
+ "coral": RGBColor(255, 127, 80),
+ "cornflowerblue": RGBColor(100, 149, 237),
+ "cornsilk": RGBColor(255, 248, 220),
+ "crimson": RGBColor(220, 20, 60),
+ "cyan": RGBColor(0, 255, 255),
+ "darkblue": RGBColor(0, 0, 139),
+ "darkcyan": RGBColor(0, 139, 139),
+ "darkgoldenrod": RGBColor(184, 134, 11),
+ "darkgray": RGBColor(169, 169, 169),
+ "darkgreen": RGBColor(0, 100, 0),
+ "darkgrey": RGBColor(169, 169, 169),
+ "darkkhaki": RGBColor(189, 183, 107),
+ "darkmagenta": RGBColor(139, 0, 139),
+ "darkolivegreen": RGBColor(85, 107, 47),
+ "darkorange": RGBColor(255, 140, 0),
+ "darkorchid": RGBColor(153, 50, 204),
+ "darkred": RGBColor(139, 0, 0),
+ "darksalmon": RGBColor(233, 150, 122),
+ "darkseagreen": RGBColor(143, 188, 143),
+ "darkslateblue": RGBColor(72, 61, 139),
+ "darkslategray": RGBColor(47, 79, 79),
+ "darkslategrey": RGBColor(47, 79, 79),
+ "darkturquoise": RGBColor(0, 206, 209),
+ "darkviolet": RGBColor(148, 0, 211),
+ "deeppink": RGBColor(255, 20, 147),
+ "deepskyblue": RGBColor(0, 191, 255),
+ "dimgray": RGBColor(105, 105, 105),
+ "dimgrey": RGBColor(105, 105, 105),
+ "dodgerblue": RGBColor(30, 144, 255),
+ "firebrick": RGBColor(178, 34, 34),
+ "floralwhite": RGBColor(255, 250, 240),
+ "forestgreen": RGBColor(34, 139, 34),
+ "fuchsia": RGBColor(255, 0, 255),
+ "gainsboro": RGBColor(220, 220, 220),
+ "ghostwhite": RGBColor(248, 248, 255),
+ "gold": RGBColor(255, 215, 0),
+ "goldenrod": RGBColor(218, 165, 32),
+ "gray": RGBColor(128, 128, 128),
+ "green": RGBColor(0, 128, 0),
+ "greenyellow": RGBColor(173, 255, 47),
+ "grey": RGBColor(128, 128, 128),
+ "honeydew": RGBColor(240, 255, 240),
+ "hotpink": RGBColor(255, 105, 180),
+ "indianred": RGBColor(205, 92, 92),
+ "indigo": RGBColor(75, 0, 130),
+ "ivory": RGBColor(255, 255, 240),
+ "khaki": RGBColor(240, 230, 140),
+ "lavender": RGBColor(230, 230, 250),
+ "lavenderblush": RGBColor(255, 240, 245),
+ "lawngreen": RGBColor(124, 252, 0),
+ "lemonchiffon": RGBColor(255, 250, 205),
+ "lightblue": RGBColor(173, 216, 230),
+ "lightcoral": RGBColor(240, 128, 128),
+ "lightcyan": RGBColor(224, 255, 255),
+ "lightgoldenrodyellow": RGBColor(250, 250, 210),
+ "lightgray": RGBColor(211, 211, 211),
+ "lightgreen": RGBColor(144, 238, 144),
+ "lightgrey": RGBColor(211, 211, 211),
+ "lightpink": RGBColor(255, 182, 193),
+ "lightsalmon": RGBColor(255, 160, 122),
+ "lightseagreen": RGBColor(32, 178, 170),
+ "lightskyblue": RGBColor(135, 206, 250),
+ "lightslategray": RGBColor(119, 136, 153),
+ "lightslategrey": RGBColor(119, 136, 153),
+ "lightsteelblue": RGBColor(176, 196, 222),
+ "lightyellow": RGBColor(255, 255, 224),
+ "lime": RGBColor(0, 255, 0),
+ "limegreen": RGBColor(50, 205, 50),
+ "linen": RGBColor(250, 240, 230),
+ "magenta": RGBColor(255, 0, 255),
+ "maroon": RGBColor(128, 0, 0),
+ "mediumaquamarine": RGBColor(102, 205, 170),
+ "mediumblue": RGBColor(0, 0, 205),
+ "mediumorchid": RGBColor(186, 85, 211),
+ "mediumpurple": RGBColor(147, 112, 219),
+ "mediumseagreen": RGBColor(60, 179, 113),
+ "mediumslateblue": RGBColor(123, 104, 238),
+ "mediumspringgreen": RGBColor(0, 250, 154),
+ "mediumturquoise": RGBColor(72, 209, 204),
+ "mediumvioletred": RGBColor(199, 21, 133),
+ "midnightblue": RGBColor(25, 25, 112),
+ "mintcream": RGBColor(245, 255, 250),
+ "mistyrose": RGBColor(255, 228, 225),
+ "moccasin": RGBColor(255, 228, 181),
+ "navajowhite": RGBColor(255, 222, 173),
+ "navy": RGBColor(0, 0, 128),
+ "navyblue": RGBColor(0, 0, 128),
+ "oldlace": RGBColor(253, 245, 230),
+ "olive": RGBColor(128, 128, 0),
+ "olivedrab": RGBColor(107, 142, 35),
+ "orange": RGBColor(255, 165, 0),
+ "orangered": RGBColor(255, 69, 0),
+ "orchid": RGBColor(218, 112, 214),
+ "palegoldenrod": RGBColor(238, 232, 170),
+ "palegreen": RGBColor(152, 251, 152),
+ "paleturquoise": RGBColor(175, 238, 238),
+ "palevioletred": RGBColor(219, 112, 147),
+ "papayawhip": RGBColor(255, 239, 213),
+ "peachpuff": RGBColor(255, 218, 185),
+ "peru": RGBColor(205, 133, 63),
+ "pink": RGBColor(255, 192, 203),
+ "plum": RGBColor(221, 160, 221),
+ "powderblue": RGBColor(176, 224, 230),
+ "purple": RGBColor(128, 0, 128),
+ "red": RGBColor(255, 0, 0),
+ "rosybrown": RGBColor(188, 143, 143),
+ "royalblue": RGBColor(65, 105, 225),
+ "saddlebrown": RGBColor(139, 69, 19),
+ "salmon": RGBColor(250, 128, 114),
+ "sandybrown": RGBColor(244, 164, 96),
+ "seagreen": RGBColor(46, 139, 87),
+ "seashell": RGBColor(255, 245, 238),
+ "sienna": RGBColor(160, 82, 45),
+ "silver": RGBColor(192, 192, 192),
+ "skyblue": RGBColor(135, 206, 235),
+ "slateblue": RGBColor(106, 90, 205),
+ "slategray": RGBColor(112, 128, 144),
+ "slategrey": RGBColor(112, 128, 144),
+ "snow": RGBColor(255, 250, 250),
+ "springgreen": RGBColor(0, 255, 127),
+ "steelblue": RGBColor(70, 130, 180),
+ "tan": RGBColor(210, 180, 140),
+ "teal": RGBColor(0, 128, 128),
+ "thistle": RGBColor(216, 191, 216),
+ "tomato": RGBColor(255, 99, 71),
+ "turquoise": RGBColor(64, 224, 208),
+ "violet": RGBColor(238, 130, 238),
+ "wheat": RGBColor(245, 222, 179),
+ "white": RGBColor(255, 255, 255),
+ "whitesmoke": RGBColor(245, 245, 245),
+ "yellow": RGBColor(255, 255, 0),
+ "yellowgreen": RGBColor(154, 205, 50),
+ # And...
+ "homeassistant": RGBColor(3, 169, 244),
+}
+
+
+@dataclass
+class XYPoint:
+ """Represents a CIE 1931 XY coordinate pair."""
+
+ x: float
+ y: float
+
+
+@dataclass
+class GamutType:
+ """Represents the Gamut of a light."""
+
+ red: XYPoint
+ green: XYPoint
+ blue: XYPoint
+
+
+def color_name_to_rgb(color_name: str) -> RGBColor:
+ """Convert color name to RGB hex value."""
+ # COLORS map has no spaces in it, so make the color_name have no
+ # spaces in it as well for matching purposes
+ hex_value = COLORS.get(color_name.replace(" ", "").lower())
+ if not hex_value:
+ msg = "Unknown color"
+ raise ValueError(msg)
+
+ return hex_value
+
+
+# pylint: disable=invalid-name
+
+
+def color_RGB_to_xy(
+ iR: int,
+ iG: int,
+ iB: int,
+ Gamut: GamutType | None = None,
+) -> tuple[float, float]:
+ """Convert from RGB color to XY color."""
+ return color_RGB_to_xy_brightness(iR, iG, iB, Gamut)[:2]
+
+
+# Taken from:
+# https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md
+# License: Code is given as is. Use at your own risk and discretion.
+def color_RGB_to_xy_brightness(
+ iR: int,
+ iG: int,
+ iB: int,
+ Gamut: GamutType | None = None,
+) -> tuple[float, float, int]:
+ """Convert from RGB color to XY color."""
+ if iR + iG + iB == 0:
+ return 0.0, 0.0, 0
+
+ R = iR / 255
+ B = iB / 255
+ G = iG / 255
+
+ # Gamma correction
+ R = pow((R + 0.055) / (1.0 + 0.055), 2.4) if (R > 0.04045) else (R / 12.92)
+ G = pow((G + 0.055) / (1.0 + 0.055), 2.4) if (G > 0.04045) else (G / 12.92)
+ B = pow((B + 0.055) / (1.0 + 0.055), 2.4) if (B > 0.04045) else (B / 12.92)
+
+ # Wide RGB D65 conversion formula
+ X = R * 0.664511 + G * 0.154324 + B * 0.162028
+ Y = R * 0.283881 + G * 0.668433 + B * 0.047685
+ Z = R * 0.000088 + G * 0.072310 + B * 0.986039
+
+ # Convert XYZ to xy
+ x = X / (X + Y + Z)
+ y = Y / (X + Y + Z)
+
+ # Brightness
+ Y = 1 if Y > 1 else Y
+ brightness = round(Y * 255)
+
+ # Check if the given xy value is within the color-reach of the lamp.
+ if Gamut:
+ in_reach = check_point_in_lamps_reach((x, y), Gamut)
+ if not in_reach:
+ xy_closest = get_closest_point_to_point((x, y), Gamut)
+ x = xy_closest[0]
+ y = xy_closest[1]
+
+ return round(x, 3), round(y, 3), brightness
+
+
+def color_xy_to_RGB(
+ vX: float,
+ vY: float,
+ Gamut: GamutType | None = None,
+) -> tuple[int, int, int]:
+ """Convert from XY to a normalized RGB."""
+ return color_xy_brightness_to_RGB(vX, vY, 255, Gamut)
+
+
+# Converted to Python from Obj-C, original source from:
+# https://github.com/PhilipsHue/PhilipsHueSDK-iOS-OSX/blob/00187a3/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md
+def color_xy_brightness_to_RGB(
+ vX: float,
+ vY: float,
+ ibrightness: int,
+ Gamut: GamutType | None = None,
+) -> tuple[int, int, int]:
+ """Convert from XYZ to RGB."""
+ if Gamut and not check_point_in_lamps_reach((vX, vY), Gamut):
+ xy_closest = get_closest_point_to_point((vX, vY), Gamut)
+ vX = xy_closest[0]
+ vY = xy_closest[1]
+
+ brightness = ibrightness / 255.0
+ if brightness == 0.0:
+ return (0, 0, 0)
+
+ Y = brightness
+
+ if vY == 0.0:
+ vY += 0.00000000001
+
+ X = (Y / vY) * vX
+ Z = (Y / vY) * (1 - vX - vY)
+
+ # Convert to RGB using Wide RGB D65 conversion.
+ r = X * 1.656492 - Y * 0.354851 - Z * 0.255038
+ g = -X * 0.707196 + Y * 1.655397 + Z * 0.036152
+ b = X * 0.051713 - Y * 0.121364 + Z * 1.011530
+
+ # Apply reverse gamma correction.
+ r, g, b = (
+ 12.92 * x if (x <= 0.0031308) else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055)
+ for x in (r, g, b)
+ )
+
+ # Bring all negative components to zero.
+ r, g, b = (max(0, x) for x in (r, g, b))
+
+ # If one component is greater than 1, weight components by that value.
+ max_component = max(r, g, b)
+ if max_component > 1:
+ r, g, b = (x / max_component for x in (r, g, b))
+
+ ir, ig, ib = (int(x * 255) for x in (r, g, b))
+
+ return (ir, ig, ib)
+
+
+def color_hsb_to_RGB(fH: float, fS: float, fB: float) -> tuple[int, int, int]:
+ """Convert a hsb into its rgb representation."""
+ if fS == 0.0:
+ fV = int(fB * 255)
+ return fV, fV, fV
+
+ r = g = b = 0
+ h = fH / 60
+ f = h - float(math.floor(h))
+ p = fB * (1 - fS)
+ q = fB * (1 - fS * f)
+ t = fB * (1 - (fS * (1 - f)))
+
+ if int(h) == 0:
+ r = int(fB * 255)
+ g = int(t * 255)
+ b = int(p * 255)
+ elif int(h) == 1:
+ r = int(q * 255)
+ g = int(fB * 255)
+ b = int(p * 255)
+ elif int(h) == 2:
+ r = int(p * 255)
+ g = int(fB * 255)
+ b = int(t * 255)
+ elif int(h) == 3:
+ r = int(p * 255)
+ g = int(q * 255)
+ b = int(fB * 255)
+ elif int(h) == 4:
+ r = int(t * 255)
+ g = int(p * 255)
+ b = int(fB * 255)
+ elif int(h) == 5:
+ r = int(fB * 255)
+ g = int(p * 255)
+ b = int(q * 255)
+
+ return (r, g, b)
+
+
+def color_RGB_to_hsv(iR: float, iG: float, iB: float) -> tuple[float, float, float]:
+ """Convert an rgb color to its hsv representation.
+
+ Hue is scaled 0-360
+ Sat is scaled 0-100
+ Val is scaled 0-100
+ """
+ fHSV = colorsys.rgb_to_hsv(iR / 255.0, iG / 255.0, iB / 255.0)
+ return round(fHSV[0] * 360, 3), round(fHSV[1] * 100, 3), round(fHSV[2] * 100, 3)
+
+
+def color_RGB_to_hs(iR: float, iG: float, iB: float) -> tuple[float, float]:
+ """Convert an rgb color to its hs representation."""
+ return color_RGB_to_hsv(iR, iG, iB)[:2]
+
+
+def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> tuple[int, int, int]:
+ """Convert an hsv color into its rgb representation.
+
+ Hue is scaled 0-360
+ Sat is scaled 0-100
+ Val is scaled 0-100
+ """
+ fRGB = colorsys.hsv_to_rgb(iH / 360, iS / 100, iV / 100)
+ return (int(fRGB[0] * 255), int(fRGB[1] * 255), int(fRGB[2] * 255))
+
+
+def color_hs_to_RGB(iH: float, iS: float) -> tuple[int, int, int]:
+ """Convert an hsv color into its rgb representation."""
+ return color_hsv_to_RGB(iH, iS, 100)
+
+
+def color_xy_to_hs(
+ vX: float,
+ vY: float,
+ Gamut: GamutType | None = None,
+) -> tuple[float, float]:
+ """Convert an xy color to its hs representation."""
+ h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY, Gamut))
+ return h, s
+
+
+def color_hs_to_xy(
+ iH: float,
+ iS: float,
+ Gamut: GamutType | None = None,
+) -> tuple[float, float]:
+ """Convert an hs color to its xy representation."""
+ return color_RGB_to_xy(*color_hs_to_RGB(iH, iS), Gamut)
+
+
+def match_max_scale(
+ input_colors: tuple[int, ...],
+ output_colors: tuple[float, ...],
+) -> tuple[int, ...]:
+ """Match the maximum value of the output to the input."""
+ max_in = max(input_colors)
+ max_out = max(output_colors)
+ factor = 0.0 if max_out == 0 else max_in / max_out
+ return tuple(int(round(i * factor)) for i in output_colors)
+
+
+def color_rgb_to_rgbw(r: int, g: int, b: int) -> tuple[int, int, int, int]:
+ """Convert an rgb color to an rgbw representation."""
+ # Calculate the white channel as the minimum of input rgb channels.
+ # Subtract the white portion from the remaining rgb channels.
+ w = min(r, g, b)
+ rgbw = (r - w, g - w, b - w, w)
+
+ # Match the output maximum value to the input. This ensures the full
+ # channel range is used.
+ return match_max_scale((r, g, b), rgbw) # type: ignore[return-value]
+
+
+def color_rgbw_to_rgb(r: int, g: int, b: int, w: int) -> tuple[int, int, int]:
+ """Convert an rgbw color to an rgb representation."""
+ # Add the white channel to the rgb channels.
+ rgb = (r + w, g + w, b + w)
+
+ # Match the output maximum value to the input. This ensures the
+ # output doesn't overflow.
+ return match_max_scale((r, g, b, w), rgb) # type: ignore[return-value]
+
+
+def color_rgb_to_rgbww(
+ r: int,
+ g: int,
+ b: int,
+ min_kelvin: int,
+ max_kelvin: int,
+) -> tuple[int, int, int, int, int]:
+ """Convert an rgb color to an rgbww representation."""
+ # Find the color temperature when both white channels have equal brightness
+ max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
+ min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
+ mired_range = max_mireds - min_mireds
+ mired_midpoint = min_mireds + mired_range / 2
+ color_temp_kelvin = color_temperature_mired_to_kelvin(mired_midpoint)
+ w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin)
+
+ # Find the ratio of the midpoint white in the input rgb channels
+ white_level = min(
+ r / w_r if w_r else 0,
+ g / w_g if w_g else 0,
+ b / w_b if w_b else 0,
+ )
+
+ # Subtract the white portion from the rgb channels.
+ rgb = (r - w_r * white_level, g - w_g * white_level, b - w_b * white_level)
+ rgbww = (*rgb, round(white_level * 255), round(white_level * 255))
+
+ # Match the output maximum value to the input. This ensures the full
+ # channel range is used.
+ return match_max_scale((r, g, b), rgbww) # type: ignore[return-value]
+
+
+def color_rgbww_to_rgb(
+ r: int,
+ g: int,
+ b: int,
+ cw: int,
+ ww: int,
+ min_kelvin: int,
+ max_kelvin: int,
+) -> tuple[int, int, int]:
+ """Convert an rgbww color to an rgb representation."""
+ # Calculate color temperature of the white channels
+ max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
+ min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
+ mired_range = max_mireds - min_mireds
+ try:
+ ct_ratio = ww / (cw + ww)
+ except ZeroDivisionError:
+ ct_ratio = 0.5
+ color_temp_mired = min_mireds + ct_ratio * mired_range
+ if color_temp_mired:
+ color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
+ else:
+ color_temp_kelvin = 0
+ w_r, w_g, w_b = color_temperature_to_rgb(color_temp_kelvin)
+ white_level = max(cw, ww) / 255
+
+ # Add the white channels to the rgb channels.
+ rgb = (r + w_r * white_level, g + w_g * white_level, b + w_b * white_level)
+
+ # Match the output maximum value to the input. This ensures the
+ # output doesn't overflow.
+ return match_max_scale((r, g, b, cw, ww), rgb) # type: ignore[return-value]
+
+
+def color_rgb_to_hex(r: int, g: int, b: int) -> str:
+ """Return a RGB color from a hex color string."""
+ return f"{round(r):02x}{round(g):02x}{round(b):02x}"
+
+
+def rgb_hex_to_rgb_list(hex_string: str) -> list[int]:
+ """Return an RGB color value list from a hex color string."""
+ return [
+ int(hex_string[i : i + len(hex_string) // 3], 16)
+ for i in range(0, len(hex_string), len(hex_string) // 3)
+ ]
+
+
+def color_temperature_to_hs(color_temperature_kelvin: float) -> tuple[float, float]:
+ """Return an hs color from a color temperature in Kelvin."""
+ return color_RGB_to_hs(*color_temperature_to_rgb(color_temperature_kelvin))
+
+
+def color_temperature_to_rgb(
+ color_temperature_kelvin: float,
+) -> tuple[float, float, float]:
+ """Return an RGB color from a color temperature in Kelvin.
+
+ This is a rough approximation based on the formula provided by T. Helland
+ http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
+ """
+ # range check
+ if color_temperature_kelvin < 1000:
+ color_temperature_kelvin = 1000
+ elif color_temperature_kelvin > 40000:
+ color_temperature_kelvin = 40000
+
+ tmp_internal = color_temperature_kelvin / 100.0
+
+ red = _get_red(tmp_internal)
+
+ green = _get_green(tmp_internal)
+
+ blue = _get_blue(tmp_internal)
+
+ return red, green, blue
+
+
+def color_temperature_to_rgbww(
+ temperature: int,
+ brightness: int,
+ min_kelvin: int,
+ max_kelvin: int,
+) -> tuple[int, int, int, int, int]:
+ """Convert color temperature in kelvin to rgbcw.
+
+ Returns a (r, g, b, cw, ww) tuple.
+ """
+ max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
+ min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
+ temperature = color_temperature_kelvin_to_mired(temperature)
+ mired_range = max_mireds - min_mireds
+ cold = ((max_mireds - temperature) / mired_range) * brightness
+ warm = brightness - cold
+ return (0, 0, 0, round(cold), round(warm))
+
+
+def rgbww_to_color_temperature(
+ rgbww: tuple[int, int, int, int, int],
+ min_kelvin: int,
+ max_kelvin: int,
+) -> tuple[int, int]:
+ """Convert rgbcw to color temperature in kelvin.
+
+ Returns a tuple (color_temperature, brightness).
+ """
+ _, _, _, cold, warm = rgbww
+ return _white_levels_to_color_temperature(cold, warm, min_kelvin, max_kelvin)
+
+
+def _white_levels_to_color_temperature(
+ cold: int,
+ warm: int,
+ min_kelvin: int,
+ max_kelvin: int,
+) -> tuple[int, int]:
+ """Convert whites to color temperature in kelvin.
+
+ Returns a tuple (color_temperature, brightness).
+ """
+ max_mireds = color_temperature_kelvin_to_mired(min_kelvin)
+ min_mireds = color_temperature_kelvin_to_mired(max_kelvin)
+ brightness = warm / 255 + cold / 255
+ if brightness == 0:
+ # Return the warmest color if brightness is 0
+ return (min_kelvin, 0)
+ return round(
+ color_temperature_mired_to_kelvin(
+ ((cold / 255 / brightness) * (min_mireds - max_mireds)) + max_mireds,
+ ),
+ ), min(255, round(brightness * 255))
+
+
+def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float:
+ """Clamp the given color component value between the given min and max values.
+
+ The range defined by the minimum and maximum values is inclusive, i.e. given a
+ color_component of 0 and a minimum of 10, the returned value is 10.
+ """
+ color_component_out = max(color_component, minimum)
+ return min(color_component_out, maximum)
+
+
+def _get_red(temperature: float) -> float:
+ """Get the red component of the temperature in RGB space."""
+ if temperature <= 66:
+ return 255
+ tmp_red = 329.698727446 * math.pow(temperature - 60, -0.1332047592)
+ return _clamp(tmp_red)
+
+
+def _get_green(temperature: float) -> float:
+ """Get the green component of the given color temp in RGB space."""
+ if temperature <= 66:
+ green = 99.4708025861 * math.log(temperature) - 161.1195681661
+ else:
+ green = 288.1221695283 * math.pow(temperature - 60, -0.0755148492)
+ return _clamp(green)
+
+
+def _get_blue(temperature: float) -> float:
+ """Get the blue component of the given color temperature in RGB space."""
+ if temperature >= 66:
+ return 255
+ if temperature <= 19:
+ return 0
+ blue = 138.5177312231 * math.log(temperature - 10) - 305.0447927307
+ return _clamp(blue)
+
+
+def color_temperature_mired_to_kelvin(mired_temperature: float) -> int:
+ """Convert absolute mired shift to degrees kelvin."""
+ return math.floor(1000000 / mired_temperature)
+
+
+def color_temperature_kelvin_to_mired(kelvin_temperature: float) -> int:
+ """Convert degrees kelvin to mired shift."""
+ return math.floor(1000000 / kelvin_temperature)
+
+
+# The following 5 functions are adapted from rgbxy provided by Benjamin Knight
+# License: The MIT License (MIT), 2014.
+# https://github.com/benknight/hue-python-rgb-converter
+def cross_product(p1: XYPoint, p2: XYPoint) -> float:
+ """Calculate the cross product of two XYPoints."""
+ return float(p1.x * p2.y - p1.y * p2.x)
+
+
+def get_distance_between_two_points(one: XYPoint, two: XYPoint) -> float:
+ """Calculate the distance between two XYPoints."""
+ dx = one.x - two.x
+ dy = one.y - two.y
+ return math.sqrt(dx * dx + dy * dy)
+
+
+def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint:
+ """Find the closest point from P to a line defined by A and B.
+
+ This point will be reproducible by the lamp
+ as it is on the edge of the gamut.
+ """
+ AP = XYPoint(P.x - A.x, P.y - A.y)
+ AB = XYPoint(B.x - A.x, B.y - A.y)
+ ab2 = AB.x * AB.x + AB.y * AB.y
+ ap_ab = AP.x * AB.x + AP.y * AB.y
+ t = ap_ab / ab2
+
+ if t < 0.0:
+ t = 0.0
+ elif t > 1.0:
+ t = 1.0
+
+ return XYPoint(A.x + AB.x * t, A.y + AB.y * t)
+
+
+def get_closest_point_to_point(
+ xy_tuple: tuple[float, float],
+ Gamut: GamutType,
+) -> tuple[float, float]:
+ """Get the closest matching color within the gamut of the light.
+
+ Should only be used if the supplied color is outside of the color gamut.
+ """
+ xy_point = XYPoint(xy_tuple[0], xy_tuple[1])
+
+ # find the closest point on each line in the CIE 1931 'triangle'.
+ pAB = get_closest_point_to_line(Gamut.red, Gamut.green, xy_point)
+ pAC = get_closest_point_to_line(Gamut.blue, Gamut.red, xy_point)
+ pBC = get_closest_point_to_line(Gamut.green, Gamut.blue, xy_point)
+
+ # Get the distances per point and see which point is closer to our Point.
+ dAB = get_distance_between_two_points(xy_point, pAB)
+ dAC = get_distance_between_two_points(xy_point, pAC)
+ dBC = get_distance_between_two_points(xy_point, pBC)
+
+ lowest = dAB
+ closest_point = pAB
+
+ if dAC < lowest:
+ lowest = dAC
+ closest_point = pAC
+
+ if dBC < lowest:
+ lowest = dBC
+ closest_point = pBC
+
+ # Change the xy value to a value which is within the reach of the lamp.
+ cx = closest_point.x
+ cy = closest_point.y
+
+ return (cx, cy)
+
+
+def check_point_in_lamps_reach(p: tuple[float, float], Gamut: GamutType) -> bool:
+ """Check if the provided XYPoint can be recreated by a Hue lamp."""
+ v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y)
+ v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y)
+
+ q = XYPoint(p[0] - Gamut.red.x, p[1] - Gamut.red.y)
+ s = cross_product(q, v2) / cross_product(v1, v2)
+ t = cross_product(v1, q) / cross_product(v1, v2)
+
+ return (s >= 0.0) and (t >= 0.0) and (s + t <= 1.0)
+
+
+def check_valid_gamut(Gamut: GamutType) -> bool:
+ """Check if the supplied gamut is valid."""
+ # Check if the three points of the supplied gamut are not on the same line.
+ v1 = XYPoint(Gamut.green.x - Gamut.red.x, Gamut.green.y - Gamut.red.y)
+ v2 = XYPoint(Gamut.blue.x - Gamut.red.x, Gamut.blue.y - Gamut.red.y)
+ not_on_line = cross_product(v1, v2) > 0.0001
+
+ # Check if all six coordinates of the gamut lie between 0 and 1.
+ red_valid = (
+ Gamut.red.x >= 0 and Gamut.red.x <= 1 and Gamut.red.y >= 0 and Gamut.red.y <= 1
+ )
+ green_valid = (
+ Gamut.green.x >= 0
+ and Gamut.green.x <= 1
+ and Gamut.green.y >= 0
+ and Gamut.green.y <= 1
+ )
+ blue_valid = (
+ Gamut.blue.x >= 0
+ and Gamut.blue.x <= 1
+ and Gamut.blue.y >= 0
+ and Gamut.blue.y <= 1
+ )
+
+ return not_on_line and red_valid and green_valid and blue_valid
diff --git a/webapp/requirements.txt b/webapp/requirements.txt
index f832a57a..ee8c6966 100644
--- a/webapp/requirements.txt
+++ b/webapp/requirements.txt
@@ -1 +1,2 @@
shinylive
+astral==2.2
From db5f9d623c25281c7bf39b767fb9043273c1a768 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 8 Aug 2023 17:34:35 -0700
Subject: [PATCH 111/549] Mention app more prominently and improve style (#725)
* Mention app more prominently
* Use dark-mode
* add setuptools
* no cyber
* no not run on PR
* Ruff fixes
* Add link
* remove unused deps
---
.github/workflows/deploy-webapp.yml | 2 -
.ruff.toml | 3 +-
README.md | 2 +
webapp/app.py | 128 +++++++++++++++++-----------
webapp/requirements.txt | 1 +
5 files changed, 81 insertions(+), 55 deletions(-)
diff --git a/.github/workflows/deploy-webapp.yml b/.github/workflows/deploy-webapp.yml
index 04504522..629902b5 100644
--- a/.github/workflows/deploy-webapp.yml
+++ b/.github/workflows/deploy-webapp.yml
@@ -5,8 +5,6 @@ on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
- pull_request:
- branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
diff --git a/.ruff.toml b/.ruff.toml
index 8ece9b9c..6ebfec62 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -26,7 +26,8 @@ ignore = [
[per-file-ignores]
"tests/*.py" = ["ALL"]
".github/*py" = ["INP001"]
-"webapp/*py" = ["ALL"]
+"webapp/homeassistant_util_color.py" = ["ALL"]
+"webapp/app.py" = ["INP001", "DTZ011", "A002"]
"custom_components/adaptive_lighting/homeassistant_util_color.py" = ["ALL"]
[flake8-pytest-style]
diff --git a/README.md b/README.md
index faa43e9b..3f9d4bc2 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,8 @@ By automatically adapting the settings of your lights throughout the day, Adapti
In addition to its regular mode, Adaptive Lighting also offers a "sleep mode" 🌜 which sets your lights to minimal brightness and a very warm color, perfect for winding down at night.
+> 🌈 Visualize Adaptive Lighting's settings with the [_🌞 Adaptive Lighting Simulator WebApp 🌛_](https://basnijholt.github.io/adaptive-lighting)
+
[[ToC](#books-table-of-contents)]
## :bulb: Features
diff --git a/webapp/app.py b/webapp/app.py
index 91a33d0c..8cc9b99f 100644
--- a/webapp/app.py
+++ b/webapp/app.py
@@ -1,28 +1,36 @@
"""Simple web app to visualize brightness over time."""
+import datetime as dt
+from contextlib import suppress
+from pathlib import Path
+from typing import Any
+
import matplotlib.pyplot as plt
import numpy as np
-from shiny import App, render, ui
-from pathlib import Path
-from contextlib import suppress
-import datetime as dt
+import shinyswatch
from astral import LocationInfo
from astral.location import Location
+from shiny import App, render, ui
-def date_range(tzinfo):
+def date_range(tzinfo: dt.tzinfo) -> list[dt.datetime]:
+ """Return a list of datetimes for the current day."""
start_of_day = dt.datetime.now(tzinfo).replace(
- hour=0, minute=0, second=0, microsecond=0
+ hour=0,
+ minute=0,
+ second=0,
+ microsecond=0,
)
# one second before the next day
end_of_day = start_of_day + dt.timedelta(days=1) - dt.timedelta(seconds=1)
hours_range = [start_of_day]
while hours_range[-1] < end_of_day:
- hours_range.append(hours_range[-1] + dt.timedelta(minutes=5))
+ hours_range.append(hours_range[-1] + dt.timedelta(minutes=1))
return hours_range[:-1]
-def copy_color_and_brightness_module():
+def copy_color_and_brightness_module() -> None:
+ """Copy the color_and_brightness module to the webapp folder."""
with suppress(Exception):
webapp_folder = Path(__file__).parent.absolute()
module = (
@@ -46,14 +54,15 @@ def copy_color_and_brightness_module():
copy_color_and_brightness_module()
-from color_and_brightness import SunLightSettings
+from color_and_brightness import SunLightSettings # noqa: E402
-def plot_brightness(kw, sleep_mode: bool):
+def plot_brightness(inputs: dict[str, Any], sleep_mode: bool):
+ """Plot the brightness over time for different modes."""
# Define the time range for our simulation
- sun_linear = SunLightSettings(**kw, brightness_mode="linear")
- sun_tanh = SunLightSettings(**kw, brightness_mode="tanh")
- sun = SunLightSettings(**kw, brightness_mode="default")
+ sun_linear = SunLightSettings(**inputs, brightness_mode="linear")
+ sun_tanh = SunLightSettings(**inputs, brightness_mode="tanh")
+ sun = SunLightSettings(**inputs, brightness_mode="default")
# Calculate the brightness for each time in the time range for all modes
dt_range = date_range(sun.timezone)
time_range = [time_to_float(dt) for dt in dt_range]
@@ -69,7 +78,7 @@ def plot_brightness(kw, sleep_mode: bool):
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(time_range, brightness_linear_values, label="Linear Mode")
ax.plot(time_range, brightness_tanh_values, label="Tanh Mode")
- ax.plot(time_range, brightness_default_values, label="Default Mode")
+ ax.plot(time_range, brightness_default_values, label="Default Mode", c="C5")
sunrise_time = sun.sun.sunrise(dt.date.today())
sunset_time = sun.sun.sunset(dt.date.today())
ax.vlines(
@@ -110,7 +119,6 @@ def plot_brightness(kw, sleep_mode: bool):
)
ax.legend()
- ax.grid(True)
ax.text(
0.4,
@@ -121,12 +129,14 @@ def plot_brightness(kw, sleep_mode: bool):
verticalalignment="center",
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.5},
)
+ ax.grid(visible=True)
return fig
-def plot_color_temp(kw, sleep_mode: bool):
- sun = SunLightSettings(**kw, brightness_mode="default")
+def plot_color_temp(inputs: dict[str, Any], sleep_mode: bool) -> plt.Figure:
+ """Plot the color temperature over time for different modes."""
+ sun = SunLightSettings(**inputs, brightness_mode="default")
dt_range = date_range(tzinfo=sun.timezone)
time_range = [time_to_float(dt) for dt in dt_range]
settings = [sun.brightness_and_color(dt, sleep_mode) for dt in dt_range]
@@ -174,16 +184,20 @@ def plot_color_temp(kw, sleep_mode: bool):
ax.legend()
ax.set_ylabel("Sun position (%)")
ax.set_title("RGB Color Intensity over Time")
-
+ ax.grid(visible=False)
return fig
SEC_PER_HR = 60 * 60
-desc = """
+desc_top = """
**Experience the Dynamics of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) in Real-Time.**
Have you ever wondered how the intricate settings of [Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) impact your home ambiance? The Adaptive Lighting Simulator WebApp is here to demystify just that.
+(More text below the plots)
+"""
+
+desc_bottom = """
Harnessing the technology of the popular Adaptive Lighting integration for Home Assistant, this webapp provides a hands-on, visual platform to explore, tweak, and understand the myriad of parameters that dictate the behavior of your smart lights. Whether you're aiming for a subtle morning glow or a cozy evening warmth, observe firsthand how each tweak changes the ambiance.
**Why Use the Simulator?**
@@ -197,17 +211,23 @@ Dive into the simulator, experiment with different settings, and fine-tune the b
# Shiny UI
app_ui = ui.page_fluid(
+ shinyswatch.theme.sandstone(),
ui.panel_title("🌞 Adaptive Lighting Simulator WebApp 🌛"),
ui.layout_sidebar(
ui.panel_sidebar(
- ui.input_switch("adapt_until_sleep", "adapt_until_sleep", False),
- ui.input_switch("sleep_mode", "sleep_mode", False),
+ ui.input_switch("adapt_until_sleep", "adapt_until_sleep", value=False),
+ ui.input_switch("sleep_mode", "sleep_mode", value=False),
ui.input_slider("min_brightness", "min_brightness", 1, 100, 30, post="%"),
ui.input_slider("max_brightness", "max_brightness", 1, 100, 100, post="%"),
ui.input_numeric("min_color_temp", "min_color_temp", 2000),
ui.input_numeric("max_color_temp", "max_color_temp", 6666),
ui.input_slider(
- "sleep_brightness", "sleep_brightness", 1, 100, 1, post="%"
+ "sleep_brightness",
+ "sleep_brightness",
+ 1,
+ 100,
+ 1,
+ post="%",
),
ui.input_radio_buttons(
"sleep_rgb_or_color_temp",
@@ -252,58 +272,62 @@ app_ui = ui.page_fluid(
),
),
ui.panel_main(
- ui.markdown(desc),
+ ui.markdown(desc_top),
ui.output_plot(id="brightness_plot"),
ui.output_plot(id="color_temp_plot"),
+ ui.markdown(desc_bottom),
),
),
)
def float_to_time(value: float) -> dt.time:
+ """Convert a float to a time object."""
hours = int(value)
minutes = int((value - hours) * 60)
- time = dt.time(hours, minutes)
- return time
+ return dt.time(hours, minutes)
def time_to_float(time: dt.time | dt.datetime) -> float:
+ """Convert a time object to a float."""
return time.hour + time.minute / 60
def _kw(input):
location = Location(LocationInfo(timezone=dt.timezone.utc))
- return dict(
- name="Adaptive Lighting Simulator",
- adapt_until_sleep=input.adapt_until_sleep(),
- max_brightness=input.max_brightness(),
- min_brightness=input.min_brightness(),
- min_color_temp=input.min_color_temp(),
- max_color_temp=input.max_color_temp(),
- sleep_brightness=input.sleep_brightness(),
- sleep_rgb_or_color_temp=input.sleep_rgb_or_color_temp(),
- sleep_color_temp=input.sleep_color_temp(),
- sleep_rgb_color=[int(x) for x in input.sleep_rgb_color().split(",")],
- sunrise_time=float_to_time(input.sunrise_time()),
- sunset_time=float_to_time(input.sunset_time()),
- brightness_mode_time_dark=dt.timedelta(
- seconds=input.brightness_mode_time_dark()
+ return {
+ "name": "Adaptive Lighting Simulator",
+ "adapt_until_sleep": input.adapt_until_sleep(),
+ "max_brightness": input.max_brightness(),
+ "min_brightness": input.min_brightness(),
+ "min_color_temp": input.min_color_temp(),
+ "max_color_temp": input.max_color_temp(),
+ "sleep_brightness": input.sleep_brightness(),
+ "sleep_rgb_or_color_temp": input.sleep_rgb_or_color_temp(),
+ "sleep_color_temp": input.sleep_color_temp(),
+ "sleep_rgb_color": [int(x) for x in input.sleep_rgb_color().split(",")],
+ "sunrise_time": float_to_time(input.sunrise_time()),
+ "sunset_time": float_to_time(input.sunset_time()),
+ "brightness_mode_time_dark": dt.timedelta(
+ seconds=input.brightness_mode_time_dark(),
),
- brightness_mode_time_light=dt.timedelta(
- seconds=input.brightness_mode_time_light()
+ "brightness_mode_time_light": dt.timedelta(
+ seconds=input.brightness_mode_time_light(),
),
- sunrise_offset=dt.timedelta(0),
- sunset_offset=dt.timedelta(0),
- min_sunrise_time=None,
- max_sunrise_time=None,
- min_sunset_time=None,
- max_sunset_time=None,
- astral_location=location,
- timezone=location.timezone,
- )
+ "sunrise_offset": dt.timedelta(0),
+ "sunset_offset": dt.timedelta(0),
+ "min_sunrise_time": None,
+ "max_sunrise_time": None,
+ "min_sunset_time": None,
+ "max_sunset_time": None,
+ "astral_location": location,
+ "timezone": location.timezone,
+ }
-def server(input, output, session):
+def server(input, output, session): # noqa: ARG001
+ """Shiny server."""
+
@output
@render.plot
def brightness_plot():
diff --git a/webapp/requirements.txt b/webapp/requirements.txt
index ee8c6966..4a287a69 100644
--- a/webapp/requirements.txt
+++ b/webapp/requirements.txt
@@ -1,2 +1,3 @@
shinylive
astral==2.2
+shinyswatch
From b06d38eb24c50fcb18747cfe7c0e6226e47cc32c Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 8 Aug 2023 18:03:06 -0700
Subject: [PATCH 112/549] Add mp4 link in README
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 3f9d4bc2..9c7fb3b2 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@ In addition to its regular mode, Adaptive Lighting also offers a "sleep mode"
> 🌈 Visualize Adaptive Lighting's settings with the [_🌞 Adaptive Lighting Simulator WebApp 🌛_](https://basnijholt.github.io/adaptive-lighting)
+https://github.com/basnijholt/adaptive-lighting/assets/6897215/68908f7d-fbf1-4991-98ce-3f2af6df996f
+
[[ToC](#books-table-of-contents)]
## :bulb: Features
From 597e050ab2a769cb9ef5fed5df9403f8d36cdb85 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 9 Aug 2023 13:54:18 -0700
Subject: [PATCH 113/549] Prevent light.turn_on of light that was just turned
off (#727)
Closes #726
---
custom_components/adaptive_lighting/switch.py | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 35b1d7e5..89106b45 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1120,6 +1120,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self.manager.reset(*self.lights)
async def _async_update_at_interval_action(self, now=None) -> None: # noqa: ARG002
+ """Update the attributes and maybe adapt the lights."""
await self._update_attrs_and_maybe_adapt_lights(
context=self.create_context("interval"),
transition=self._transition,
@@ -1331,7 +1332,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
data,
)
- async def _update_attrs_and_maybe_adapt_lights(
+ async def _update_attrs_and_maybe_adapt_lights( # noqa: PLR0912
self,
*,
context: Context,
@@ -1379,6 +1380,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._name,
light,
)
+ elif (
+ # This is to prevent lights immediately turning on after
+ # being turned off in 'interval' update, see #726
+ not self._detect_non_ha_changes
+ and is_our_context(context, "interval")
+ and (turn_on := self.manager.turn_on_event.get(light))
+ and (turn_off := self.manager.turn_off_event.get(light))
+ and turn_off.time_fired > turn_on.time_fired
+ ):
+ _LOGGER.debug(
+ "%s: Light '%s' was turned just turned off",
+ self._name,
+ light,
+ )
else:
filtered_lights.append(light)
From da5147a58367fe278bdd0ae1e67f9ef80de3996e Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 9 Aug 2023 13:56:00 -0700
Subject: [PATCH 114/549] Bump to 1.19.0b5 in manifest.json (#728)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index 3b6c7cf5..bfbe994b 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.19.0b4"
+ "version": "1.19.0b5"
}
From 435b2ce5d0c82b24659bf295999cc76b2f48a2c4 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 9 Aug 2023 14:48:18 -0700
Subject: [PATCH 115/549] Fix adapt_only_on_bare_turn_on and apply does not
result in manual_control (#729)
* Fix adapt_only_on_bare_turn_on and apply does not result in manual_control
Closes #723
* fix style
---
custom_components/adaptive_lighting/switch.py | 8 ++++++--
tests/test_switch.py | 16 ++++++++++++++++
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 89106b45..150f2ef4 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1376,9 +1376,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
timer = self.manager.transition_timers.get(light)
if timer is not None and timer.is_running():
_LOGGER.debug(
- "%s: Light '%s' is still transitioning",
+ "%s: Light '%s' is still transitioning, context.id='%s'",
self._name,
light,
+ context.id,
)
elif (
# This is to prevent lights immediately turning on after
@@ -1390,9 +1391,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
and turn_off.time_fired > turn_on.time_fired
):
_LOGGER.debug(
- "%s: Light '%s' was turned just turned off",
+ "%s: Light '%s' was turned just turned off, context.id='%s'",
self._name,
light,
+ context.id,
)
else:
filtered_lights.append(light)
@@ -1489,6 +1491,8 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._take_over_control
and self._adapt_only_on_bare_turn_on
and from_turn_on
+ # adaptive_lighting.apply can turn on light, so check this is not our context
+ and not is_our_context(event.context)
):
service_data = self.manager.turn_on_event[entity_id].data[ATTR_SERVICE_DATA]
if self.manager._mark_manual_control_if_non_bare_turn_on(
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 6ffd1a21..4744f878 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -740,6 +740,22 @@ async def test_manual_control(
await change_manual_control(False, {})
assert all([not manual_control[eid] for eid in switch.lights])
+ # Turn off light and turn on using adaptive_lighting.apply
+ await turn_light(False)
+ await hass.services.async_call(
+ DOMAIN,
+ SERVICE_APPLY,
+ {
+ ATTR_ENTITY_ID: ENTITY_SWITCH,
+ CONF_LIGHTS: [ENTITY_LIGHT_1],
+ CONF_TURN_ON_LIGHTS: True,
+ },
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON
+ assert not manual_control[ENTITY_LIGHT_1]
+
async def test_auto_reset_manual_control(hass):
switch, (light, *_) = await setup_lights_and_switch(
From 0d387d4bcd4709d73d5640a40e24fd321a831f7d Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 9 Aug 2023 14:56:08 -0700
Subject: [PATCH 116/549] Add release-drafter GitHub Action (#731)
---
.github/workflows/release-drafter.yml | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 .github/workflows/release-drafter.yml
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
new file mode 100644
index 00000000..12b1f6c2
--- /dev/null
+++ b/.github/workflows/release-drafter.yml
@@ -0,0 +1,22 @@
+name: Release Drafter
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [opened, reopened, synchronize]
+
+permissions:
+ contents: read
+
+jobs:
+ update_release_draft:
+ permissions:
+ contents: write
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: release-drafter/release-drafter@v5
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
From 750acc978423dbcc41c998ee18c167c452ff9426 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 9 Aug 2023 14:57:34 -0700
Subject: [PATCH 117/549] Add release-drafter config
---
.github/release-drafter.yml | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 .github/release-drafter.yml
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 00000000..27bcee3f
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,4 @@
+template: |
+ ## What’s Changed
+
+ $CHANGES
From 487756b345b4be286fc2118eb0eaf78d885e4ea2 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 9 Aug 2023 15:01:00 -0700
Subject: [PATCH 118/549] Add full changelog to releases
---
.github/release-drafter.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
index 27bcee3f..ad38c0ea 100644
--- a/.github/release-drafter.yml
+++ b/.github/release-drafter.yml
@@ -2,3 +2,5 @@ template: |
## What’s Changed
$CHANGES
+
+ **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
From 5b7ee446295a5ff54dcfc54ba7e34cce0d271237 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 9 Aug 2023 17:17:25 -0700
Subject: [PATCH 119/549] Fix length of descriptions in UI (config flow) (#732)
* Fix length of strings in UI
* Update README.md, strings.json, and services.yaml
* fix
* add desc
* Update README.md, strings.json, and services.yaml
* mention webapp
* Add desc
* link
---------
Co-authored-by: github-actions[bot]
---
.github/update-strings.py | 13 +++-
.../adaptive_lighting/strings.json | 70 +++++++++++++------
.../adaptive_lighting/translations/en.json | 70 +++++++++++++------
3 files changed, 106 insertions(+), 47 deletions(-)
diff --git a/.github/update-strings.py b/.github/update-strings.py
index 44dc6bd6..f90ac437 100644
--- a/.github/update-strings.py
+++ b/.github/update-strings.py
@@ -3,6 +3,7 @@ import json
import sys
from pathlib import Path
+import homeassistant.helpers.config_validation as cv
import yaml
sys.path.append(str(Path(__file__).parent.parent))
@@ -16,8 +17,17 @@ with strings_fname.open() as f:
strings = json.load(f)
# Set "options"
-data = {k: f"{k}: {const.DOCS[k]}" for k, _, _ in const.VALIDATION_TUPLES}
+data = {}
+data_description = {}
+for k, _, typ in const.VALIDATION_TUPLES:
+ desc = const.DOCS[k]
+ if len(desc) > 40 and typ != bool and typ != cv.entity_ids:
+ data[k] = k
+ data_description[k] = desc
+ else:
+ data[k] = f"{k}: {desc}"
strings["options"]["step"]["init"]["data"] = data
+strings["options"]["step"]["init"]["data_description"] = data_description
# Set "services"
services_filename = Path("custom_components") / "adaptive_lighting" / "services.yaml"
@@ -48,6 +58,7 @@ with en_fname.open() as f:
en["config"]["step"]["user"] = strings["config"]["step"]["user"]
en["options"]["step"]["init"]["data"] = data
+en["options"]["step"]["init"]["data_description"] = data_description
en["services"] = services_json
with en_fname.open("w") as f:
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index 5e60ed79..5733bfd5 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -17,45 +17,69 @@
"step": {
"init": {
"title": "Adaptive Lighting options",
- "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have this entry defined in YAML.",
+ "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app](https://basnijholt.github.io/adaptive-lighting). For further details, see the [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).",
"data": {
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
- "interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
- "transition": "transition: Duration of transition when lights change, in seconds. 🕑",
- "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
+ "interval": "interval",
+ "transition": "transition",
+ "initial_transition": "initial_transition",
"min_brightness": "min_brightness: Minimum brightness percentage. 💡",
"max_brightness": "max_brightness: Maximum brightness percentage. 💡",
"min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥",
"max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️",
"prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
- "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
- "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
- "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
- "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
+ "sleep_brightness": "sleep_brightness",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp",
+ "sleep_color_temp": "sleep_color_temp",
+ "sleep_rgb_color": "sleep_rgb_color",
+ "sleep_transition": "sleep_transition",
"transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
- "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
- "min_sunrise_time": "min_sunrise_time: Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
- "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
- "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
- "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
- "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
- "max_sunset_time": "max_sunset_time: Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇",
- "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
- "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
- "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
- "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
+ "sunrise_time": "sunrise_time",
+ "min_sunrise_time": "min_sunrise_time",
+ "max_sunrise_time": "max_sunrise_time",
+ "sunrise_offset": "sunrise_offset",
+ "sunset_time": "sunset_time",
+ "min_sunset_time": "min_sunset_time",
+ "max_sunset_time": "max_sunset_time",
+ "sunset_offset": "sunset_offset",
+ "brightness_mode": "brightness_mode",
+ "brightness_mode_time_dark": "brightness_mode_time_dark",
+ "brightness_mode_time_light": "brightness_mode_time_light",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
- "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "autoreset_control_seconds": "autoreset_control_seconds",
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
- "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
- "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
+ "send_split_delay": "send_split_delay",
+ "adapt_delay": "adapt_delay",
"skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.",
"multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.",
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝"
+ },
+ "data_description": {
+ "interval": "Frequency to adapt the lights, in seconds. 🔄",
+ "transition": "Duration of transition when lights change, in seconds. 🕑",
+ "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
+ "sleep_brightness": "Brightness percentage of lights in sleep mode. 😴",
+ "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
+ "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
+ "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
+ "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
+ "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅",
+ "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
+ "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
+ "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰",
+ "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇",
+ "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
+ "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇",
+ "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰",
+ "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
+ "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
+ "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
+ "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
+ "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️"
}
}
},
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index cbf87f96..eaf28798 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -18,45 +18,69 @@
"step": {
"init": {
"title": "Adaptive Lighting options",
- "description": "All settings for a Adaptive Lighting component. The option names correspond with the YAML settings. No options are shown if you have the adaptive_lighting entry defined in your YAML configuration.",
+ "description": "Configure an Adaptive Lighting component. Option names align with the YAML settings. If you've defined this entry in YAML, no options will appear here. For interactive graphs that demonstrate parameter effects, visit [this web app](https://basnijholt.github.io/adaptive-lighting). For further details, see the [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).",
"data": {
"lights": "lights: List of light entity_ids to be controlled (may be empty). 🌟",
- "interval": "interval: Frequency to adapt the lights, in seconds. 🔄",
- "transition": "transition: Duration of transition when lights change, in seconds. 🕑",
- "initial_transition": "initial_transition: Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
+ "interval": "interval",
+ "transition": "transition",
+ "initial_transition": "initial_transition",
"min_brightness": "min_brightness: Minimum brightness percentage. 💡",
"max_brightness": "max_brightness: Maximum brightness percentage. 💡",
"min_color_temp": "min_color_temp: Warmest color temperature in Kelvin. 🔥",
"max_color_temp": "max_color_temp: Coldest color temperature in Kelvin. ❄️",
"prefer_rgb_color": "prefer_rgb_color: Whether to prefer RGB color adjustment over light color temperature when possible. 🌈",
- "sleep_brightness": "sleep_brightness: Brightness percentage of lights in sleep mode. 😴",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp: Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
- "sleep_color_temp": "sleep_color_temp: Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
- "sleep_rgb_color": "sleep_rgb_color: RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
- "sleep_transition": "sleep_transition: Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
+ "sleep_brightness": "sleep_brightness",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp",
+ "sleep_color_temp": "sleep_color_temp",
+ "sleep_rgb_color": "sleep_rgb_color",
+ "sleep_transition": "sleep_transition",
"transition_until_sleep": "transition_until_sleep: When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. 🌙",
- "sunrise_time": "sunrise_time: Set a fixed time (HH:MM:SS) for sunrise. 🌅",
- "min_sunrise_time": "min_sunrise_time: Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
- "max_sunrise_time": "max_sunrise_time: Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
- "sunrise_offset": "sunrise_offset: Adjust sunrise time with a positive or negative offset in seconds. ⏰",
- "sunset_time": "sunset_time: Set a fixed time (HH:MM:SS) for sunset. 🌇",
- "min_sunset_time": "min_sunset_time: Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
- "max_sunset_time": "max_sunset_time: Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇",
- "sunset_offset": "sunset_offset: Adjust sunset time with a positive or negative offset in seconds. ⏰",
- "brightness_mode": "brightness_mode: Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
- "brightness_mode_time_dark": "brightness_mode_time_dark: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
- "brightness_mode_time_light": "brightness_mode_time_light: (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
+ "sunrise_time": "sunrise_time",
+ "min_sunrise_time": "min_sunrise_time",
+ "max_sunrise_time": "max_sunrise_time",
+ "sunrise_offset": "sunrise_offset",
+ "sunset_time": "sunset_time",
+ "min_sunset_time": "min_sunset_time",
+ "max_sunset_time": "max_sunset_time",
+ "sunset_offset": "sunset_offset",
+ "brightness_mode": "brightness_mode",
+ "brightness_mode_time_dark": "brightness_mode_time_dark",
+ "brightness_mode_time_light": "brightness_mode_time_light",
"take_over_control": "take_over_control: Disable Adaptive Lighting if another source calls `light.turn_on` while lights are on and being adapted. Note that this calls `homeassistant.update_entity` every `interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. 🕵️ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Disable this feature if you encounter such issues.",
- "autoreset_control_seconds": "autoreset_control_seconds: Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "autoreset_control_seconds": "autoreset_control_seconds",
"only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄",
"adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. 🕵️ ",
"separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. 🔀",
- "send_split_delay": "send_split_delay: Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
- "adapt_delay": "adapt_delay: Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️",
+ "send_split_delay": "send_split_delay",
+ "adapt_delay": "adapt_delay",
"skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.",
"multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.",
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝"
+ },
+ "data_description": {
+ "interval": "Frequency to adapt the lights, in seconds. 🔄",
+ "transition": "Duration of transition when lights change, in seconds. 🕑",
+ "initial_transition": "Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️",
+ "sleep_brightness": "Brightness percentage of lights in sleep mode. 😴",
+ "sleep_rgb_or_color_temp": "Use either `\"rgb_color\"` or `\"color_temp\"` in sleep mode. 🌙",
+ "sleep_color_temp": "Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴",
+ "sleep_rgb_color": "RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is \"rgb_color\"). 🌈",
+ "sleep_transition": "Duration of transition when \"sleep mode\" is toggled in seconds. 😴",
+ "sunrise_time": "Set a fixed time (HH:MM:SS) for sunrise. 🌅",
+ "min_sunrise_time": "Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. 🌅",
+ "max_sunrise_time": "Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. 🌅",
+ "sunrise_offset": "Adjust sunrise time with a positive or negative offset in seconds. ⏰",
+ "sunset_time": "Set a fixed time (HH:MM:SS) for sunset. 🌇",
+ "min_sunset_time": "Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. 🌇",
+ "max_sunset_time": "Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. 🌇",
+ "sunset_offset": "Adjust sunset time with a positive or negative offset in seconds. ⏰",
+ "brightness_mode": "Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). 📈",
+ "brightness_mode_time_dark": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. 📈📉",
+ "brightness_mode_time_light": "(Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. 📈📉.",
+ "autoreset_control_seconds": "Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️",
+ "send_split_delay": "Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️",
+ "adapt_delay": "Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️"
}
}
},
From 9112bb9f7329f8aee995ac33b9502ce83b8177f8 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Wed, 9 Aug 2023 19:06:48 -0700
Subject: [PATCH 120/549] Bump to 1.19.0 in manifest.json (#734)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index bfbe994b..eb7f0155 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.19.0b5"
+ "version": "1.19.0"
}
From 0723280a89678834f3b4c0afc2112e74cdf593d0 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 10 Aug 2023 17:00:16 -0700
Subject: [PATCH 121/549] Fix sleep_mode + sleep_rgb_or_color_temp ==
"color_temp" in webapp (#740)
Thanks to @danielbrunt57 for reporting here
---
webapp/app.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/webapp/app.py b/webapp/app.py
index 8cc9b99f..ab896687 100644
--- a/webapp/app.py
+++ b/webapp/app.py
@@ -10,6 +10,7 @@ import numpy as np
import shinyswatch
from astral import LocationInfo
from astral.location import Location
+from homeassistant_util_color import color_temperature_to_rgb
from shiny import App, render, ui
@@ -140,9 +141,14 @@ def plot_color_temp(inputs: dict[str, Any], sleep_mode: bool) -> plt.Figure:
dt_range = date_range(tzinfo=sun.timezone)
time_range = [time_to_float(dt) for dt in dt_range]
settings = [sun.brightness_and_color(dt, sleep_mode) for dt in dt_range]
- color_temp_values = (
- np.array([(*setting["rgb_color"], 255) for setting in settings]) / 255
- )
+ if sleep_mode and sun.sleep_rgb_or_color_temp == "color_temp":
+ colors = [
+ color_temperature_to_rgb(setting["color_temp_kelvin"])
+ for setting in settings
+ ]
+ else:
+ colors = [setting["rgb_color"] for setting in settings]
+ color_temp_values = np.array([(*col, 255) for col in colors]) / 255
color_temp_values = color_temp_values.reshape(-1, 1, 4)
sun_position = [setting["sun_position"] for setting in settings]
fig, ax = plt.subplots(figsize=(10, 6))
From f881b4b32d1eb2913a133e4476181b36900a9d3f Mon Sep 17 00:00:00 2001
From: Kendell R
Date: Mon, 14 Aug 2023 16:41:34 -0400
Subject: [PATCH 122/549] Use new logo (#746)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 9c7fb3b2..39eafa88 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
-
+
[Adaptive Lighting](https://github.com/basnijholt/adaptive-lighting) is a custom component for [Home Assistant](https://www.home-assistant.io/) that intelligently adjusts the brightness and color of your lights 💡 based on the sun's position, while still allowing for manual control.
From f540057093cff6fa74dcfda00037f9c793e34cac Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Mon, 14 Aug 2023 13:49:15 -0700
Subject: [PATCH 123/549] docs: add KTibow as a contributor for design (#747)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 12 +++++++++++-
README.md | 3 ++-
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 33b875f2..c264a714 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -449,8 +449,18 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "KTibow",
+ "name": "Kendell R",
+ "avatar_url": "https://avatars.githubusercontent.com/u/10727862?v=4",
+ "profile": "https://ktibow.github.io/",
+ "contributions": [
+ "design"
+ ]
}
],
"contributorsPerLine": 7,
- "linkToUsage": true
+ "linkToUsage": true,
+ "commitType": "docs"
}
diff --git a/README.md b/README.md
index 39eafa88..dff1f89c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -521,6 +521,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Raman Gupta 💻 |
 igiannakas 💻 |
 Mario Guggenberger 💻 |
+  Kendell R 🎨 |
From 68feb3d93164876ce4829dd2bdc53907ffddee5e Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 15 Aug 2023 12:23:56 -0700
Subject: [PATCH 124/549] Make instantaneous `light.turn_on` adaptation
configurable with `intercept` (#750)
* Make intercept configurable
* Update README.md, strings.json, and services.yaml
* import
* skip
* split
* do not pass config_entry
* add to conf
* Update README.md, strings.json, and services.yaml
* spacing
---------
Co-authored-by: github-actions[bot]
---
README.md | 3 +-
custom_components/adaptive_lighting/const.py | 11 ++-
.../adaptive_lighting/strings.json | 3 +-
custom_components/adaptive_lighting/switch.py | 78 +++++++++----------
.../adaptive_lighting/translations/en.json | 3 +-
tests/test_switch.py | 24 +++---
6 files changed, 65 insertions(+), 57 deletions(-)
diff --git a/README.md b/README.md
index dff1f89c..1f2065c7 100644
--- a/README.md
+++ b/README.md
@@ -138,7 +138,8 @@ The YAML and frontend configuration methods support all of the options listed be
| `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` |
| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` |
-| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. | `True` | `bool` |
+| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` |
+| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` |
| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. 📝 | `False` | `bool` |
diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py
index c747528d..cf93cee6 100644
--- a/custom_components/adaptive_lighting/const.py
+++ b/custom_components/adaptive_lighting/const.py
@@ -231,6 +231,13 @@ DOCS[CONF_SKIP_REDUNDANT_COMMANDS] = (
"Disable if physical light states get out of sync with HA's recorded state."
)
+CONF_INTERCEPT, DEFAULT_INTERCEPT = "intercept", True
+DOCS[CONF_INTERCEPT] = (
+ "Intercept and adapt `light.turn_on` calls to enabling instantaneous color "
+ "and brightness adaptation. 🏎️ Disable for lights that do not "
+ "support `light.turn_on` with color and brightness."
+)
+
CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = (
"multi_light_intercept",
True,
@@ -238,7 +245,8 @@ CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT = (
DOCS[CONF_MULTI_LIGHT_INTERCEPT] = (
"Intercept and adapt `light.turn_on` calls that target multiple lights. ➗"
"⚠️ This might result in splitting up a single `light.turn_on` call "
- "into multiple calls, e.g., when lights are in different switches."
+ "into multiple calls, e.g., when lights are in different switches. "
+ "Requires `intercept` to be enabled."
)
SLEEP_MODE_SWITCH = "sleep_mode_switch"
@@ -356,6 +364,7 @@ VALIDATION_TUPLES = [
DEFAULT_SKIP_REDUNDANT_COMMANDS,
bool,
),
+ (CONF_INTERCEPT, DEFAULT_INTERCEPT, bool),
(CONF_MULTI_LIGHT_INTERCEPT, DEFAULT_MULTI_LIGHT_INTERCEPT, bool),
(CONF_INCLUDE_CONFIG_IN_ATTRIBUTES, DEFAULT_INCLUDE_CONFIG_IN_ATTRIBUTES, bool),
]
diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json
index 5733bfd5..f6f6890b 100644
--- a/custom_components/adaptive_lighting/strings.json
+++ b/custom_components/adaptive_lighting/strings.json
@@ -54,7 +54,8 @@
"send_split_delay": "send_split_delay",
"adapt_delay": "adapt_delay",
"skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.",
- "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.",
+ "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.",
+ "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.",
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝"
},
"data_description": {
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 150f2ef4..4d66cc3d 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -107,6 +107,7 @@ from .const import (
CONF_DETECT_NON_HA_CHANGES,
CONF_INCLUDE_CONFIG_IN_ATTRIBUTES,
CONF_INITIAL_TRANSITION,
+ CONF_INTERCEPT,
CONF_INTERVAL,
CONF_LIGHTS,
CONF_MANUAL_CONTROL,
@@ -180,11 +181,6 @@ _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=10)
-# A (non-user-configurable, thus internal) flag to control the proactive adaptation mode.
-# This exists to disable the proactive adaptation in the unit tests and enable it
-# only for specific unit tests and when running as integration."""
-INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION = "proactive_adaptation"
-
# Consider it a significant change when attribute changes more than
BRIGHTNESS_CHANGE = 25 # ≈10% of total range
COLOR_TEMP_CHANGE = 100 # ≈3% of total range (2000-6500)
@@ -427,7 +423,7 @@ async def async_setup_entry( # noqa: PLR0915
return
if (manager := data.get(ATTR_ADAPTIVE_LIGHTING_MANAGER)) is None:
- manager = AdaptiveLightingManager(hass, config_entry)
+ manager = AdaptiveLightingManager(hass)
data[ATTR_ADAPTIVE_LIGHTING_MANAGER] = manager
sleep_mode_switch = SimpleSwitch(
@@ -882,7 +878,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON]
self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL]
self._skip_redundant_commands = data[CONF_SKIP_REDUNDANT_COMMANDS]
+ self._intercept = data[CONF_INTERCEPT]
self._multi_light_intercept = data[CONF_MULTI_LIGHT_INTERCEPT]
+ if not data[CONF_INTERCEPT] and data[CONF_MULTI_LIGHT_INTERCEPT]:
+ _LOGGER.warning(
+ "%s: Config mismatch: `multi_light_intercept` set to `true` requires `intercept`"
+ " to be enabled. Adjusting config and continuing setup with"
+ " `multi_light_intercept: false`.",
+ self._name,
+ )
+ self._multi_light_intercept = False
self._expand_light_groups() # updates manual control timers
location, _ = get_astral_location(self.hass)
@@ -1603,11 +1608,10 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
class AdaptiveLightingManager:
"""Track 'light.turn_off' and 'light.turn_on' service calls."""
- def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
+ def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the AdaptiveLightingManager that is shared among all switches."""
assert hass is not None
self.hass = hass
- data = validate(config_entry)
self.lights: set[str] = set()
# Tracks 'light.turn_off' service calls
@@ -1658,38 +1662,32 @@ class AdaptiveLightingManager:
self._proactively_adapting_contexts: dict[str, str] = {}
- is_proactive_adaptation_enabled = data.get(
- INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION,
- True,
- )
+ try:
+ self.listener_removers.append(
+ setup_service_call_interceptor(
+ hass,
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ self._service_interceptor_turn_on_handler,
+ ),
+ )
- if is_proactive_adaptation_enabled:
- try:
- self.listener_removers.append(
- setup_service_call_interceptor(
- hass,
- LIGHT_DOMAIN,
- SERVICE_TURN_ON,
- self._service_interceptor_turn_on_handler,
- ),
- )
+ self.listener_removers.append(
+ setup_service_call_interceptor(
+ hass,
+ LIGHT_DOMAIN,
+ SERVICE_TOGGLE,
+ self._service_interceptor_turn_on_handler,
+ ),
+ )
- self.listener_removers.append(
- setup_service_call_interceptor(
- hass,
- LIGHT_DOMAIN,
- SERVICE_TOGGLE,
- self._service_interceptor_turn_on_handler,
- ),
- )
-
- _LOGGER.debug("Proactive adaptation enabled")
- except RuntimeError:
- _LOGGER.warning(
- "Failed to set up service call interceptors, "
- "falling back to event-reactive mode",
- exc_info=True,
- )
+ _LOGGER.debug("Proactive adaptation enabled")
+ except RuntimeError:
+ _LOGGER.warning(
+ "Failed to set up service call interceptors, "
+ "falling back to event-reactive mode",
+ exc_info=True,
+ )
def disable(self):
"""Disable the listener by removing all subscribed handlers."""
@@ -1812,6 +1810,7 @@ class AdaptiveLightingManager:
else:
if (
not switch.is_on
+ or not switch._intercept
# Never adapt on light groups, because HA will make a separate light.turn_on
or _is_light_group(self.hass.states.get(entity_id))
# Prevent adaptation of TURN_ON calls when light is already on,
@@ -1829,12 +1828,13 @@ class AdaptiveLightingManager:
):
_LOGGER.debug(
"Switch is off or light is already on for entity_id='%s', skipped='%s'"
- " (is_on='%s', is_state='%s', manual_control='%s')",
+ " (is_on='%s', is_state='%s', manual_control='%s', switch._intercept='%s')",
entity_id,
skipped,
switch.is_on,
self.hass.states.is_state(entity_id, STATE_ON),
self.manual_control.get(entity_id, False),
+ switch._intercept,
)
skipped.append(entity_id)
else:
diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json
index eaf28798..f55a6d88 100644
--- a/custom_components/adaptive_lighting/translations/en.json
+++ b/custom_components/adaptive_lighting/translations/en.json
@@ -55,7 +55,8 @@
"send_split_delay": "send_split_delay",
"adapt_delay": "adapt_delay",
"skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. 📉Disable if physical light states get out of sync with HA's recorded state.",
- "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches.",
+ "intercept": "intercept: Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness.",
+ "multi_light_intercept": "multi_light_intercept: Intercept and adapt `light.turn_on` calls that target multiple lights. ➗⚠️ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled.",
"include_config_in_attributes": "include_config_in_attributes: Show all options as attributes on the switch in Home Assistant when set to `true`. 📝"
},
"data_description": {
diff --git a/tests/test_switch.py b/tests/test_switch.py
index 4744f878..018c8965 100644
--- a/tests/test_switch.py
+++ b/tests/test_switch.py
@@ -92,7 +92,7 @@ from custom_components.adaptive_lighting.const import (
UNDO_UPDATE_LISTENER,
)
from custom_components.adaptive_lighting.switch import (
- INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION,
+ CONF_INTERCEPT,
AdaptiveSwitch,
_attributes_have_changed,
color_difference_redmean,
@@ -167,7 +167,7 @@ async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitc
domain=DOMAIN,
data={
CONF_NAME: DEFAULT_NAME,
- INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: False,
+ CONF_INTERCEPT: False,
**extra_data,
},
)
@@ -579,7 +579,7 @@ async def test_manual_control(
hass,
{
CONF_ADAPT_ONLY_ON_BARE_TURN_ON: adapt_only_on_bare_turn_on,
- INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation,
+ CONF_INTERCEPT: proactive_service_call_adaptation,
},
)
assert switch._take_over_control
@@ -1470,9 +1470,7 @@ def _mock_sun_light_settings(switch: AdaptiveSwitch, settings: dict[str, Any]):
async def test_proactive_adaptation(hass):
"""Validate that a proactive adaptation updates the original service call."""
- switch, _ = await setup_lights_and_switch(
- hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True
- )
+ switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True)
_mock_sun_light_settings(
switch,
@@ -1503,7 +1501,7 @@ async def test_proactive_adaptation_with_separate_commands(hass):
switch, _ = await setup_lights_and_switch(
hass,
{
- INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True,
+ CONF_INTERCEPT: True,
CONF_SEPARATE_TURN_ON_COMMANDS: True,
},
True,
@@ -1539,9 +1537,7 @@ async def test_proactive_adaptation_toggle(hass):
This test is based on the fact that contexts of proactive adaptations are recorded.
"""
- switch, _ = await setup_lights_and_switch(
- hass, {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}, True
- )
+ switch, _ = await setup_lights_and_switch(hass, {CONF_INTERCEPT: True}, True)
# Toggle ON
await hass.services.async_call(
@@ -1571,7 +1567,7 @@ async def test_proactive_adaptation_transition_override(hass):
switch, (_, _, light3) = await setup_lights_and_switch(
hass,
{
- INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True,
+ CONF_INTERCEPT: True,
CONF_INITIAL_TRANSITION: 123,
},
True,
@@ -1628,7 +1624,7 @@ async def setup_proactive_multiple_lights_two_switches(hass):
CONF_DETECT_NON_HA_CHANGES: True,
CONF_PREFER_RGB_COLOR: False,
CONF_MIN_COLOR_TEMP: 2500, # to not coincide with sleep_color_temp}
- INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True,
+ CONF_INTERCEPT: True,
}
_, switch1 = await setup_switch(
hass, {CONF_NAME: "switch1", CONF_LIGHTS: [ENTITY_LIGHT_1], **defaults}
@@ -1753,7 +1749,7 @@ async def test_two_switches_for_single_light(hass):
One switch for brightness and another for color.
"""
- extra_conf = {INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: True}
+ extra_conf = {CONF_INTERCEPT: True}
switch1, (light1, *_) = await setup_lights_and_switch(
hass, extra_conf | {CONF_NAME: "switch1"}, all_lights=True
)
@@ -1928,7 +1924,7 @@ async def test_light_group(
hass,
{
CONF_LIGHTS: entity_ids,
- INTERNAL_CONF_PROACTIVE_SERVICE_CALL_ADAPTATION: proactive_service_call_adaptation,
+ CONF_INTERCEPT: proactive_service_call_adaptation,
CONF_TAKE_OVER_CONTROL: take_over_control,
CONF_MULTI_LIGHT_INTERCEPT: multi_light_intercept,
},
From e9b7988868e2c9e0b35e02816a35707ba0f3e520 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 15 Aug 2023 12:52:39 -0700
Subject: [PATCH 125/549] Fix skipped path in multi light intercept (#751)
* Fix skipped path in multi light intercept
* Deepcopy to make sure that _service_interceptor_turn_on_single_light_handler doesn't moddify
* fix
---
custom_components/adaptive_lighting/switch.py | 163 +++++++++++-------
1 file changed, 96 insertions(+), 67 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 4d66cc3d..5f69dd5f 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -1726,62 +1726,11 @@ class AdaptiveLightingManager:
for key in keys:
self._proactively_adapting_contexts.pop(key)
- async def _service_interceptor_turn_on_handler( # noqa: PLR0912, PLR0915
+ def _separate_entity_ids(
self,
- call: ServiceCall,
- data: ServiceData,
- ) -> None:
- """Intercept `light.turn_on` and `light.toggle` service calls and adapt them.
-
- It is possible that the calls are made for multiple lights at once,
- which in turn might be in different switches or no switches at all.
- If there are lights that are not all in a single switch, we need to
- make multiple calls to `light.turn_on` with the correct entity IDs.
- One of these calls can be intercepted and adapted, the others need to
- be adapted by calling `_adapt_light` with the correct entity IDs or
- by calling `light.turn_on` directly.
-
- We create a mapping from switch to entity IDs and keep a list
- of skipped lights which are lights in no switches or in switches that
- are off or lights that are already on.
-
- If there is only one switch and 0 skipped lights, we just intercept the
- call directly.
-
- If there are multiple switches and skipped lights, we can adapt the call
- for one of the switches to include only the lights in that switch and
- need to call `_adapt_light` for the other switches with their
- entity_ids. For skipped lights, we call light.turn_on directly with the
- entity_ids and original service data.
-
- If there are only skipped lights, we can use the intercepted call
- directly.
- """
- is_skipped_hash = is_our_context(call.context, "skipped")
- _LOGGER.debug(
- "(0) _service_interceptor_turn_on_handler: call.context.id='%s', is_skipped_hash='%s'",
- call.context.id,
- is_skipped_hash,
- )
- if is_our_context(call.context) and not is_skipped_hash:
- # Don't adapt our own service calls, but do re-adapt calls that
- # were skipped by us
- return
-
- if ATTR_EFFECT in data[CONF_PARAMS] or ATTR_FLASH in data[CONF_PARAMS]:
- return
-
- _LOGGER.debug(
- "(1) _service_interceptor_turn_on_handler: call='%s', data='%s'",
- call,
- data,
- )
-
- entity_ids = self._get_entity_list(data)
- # Note: we do not expand light groups anywhere in this method, instead
- # we skip them and rely on the followup call that HA will make
- # with the expanded entity IDs.
-
+ entity_ids: list[str],
+ data,
+ ) -> tuple[list[str], list[str]]:
# Create a mapping from switch to entity IDs
# AdaptiveSwitch.name → entity_ids mapping
switch_to_eids: dict[str, list[str]] = {}
@@ -1840,7 +1789,15 @@ class AdaptiveLightingManager:
else:
switch_to_eids.setdefault(switch.name, []).append(entity_id)
switch_name_mapping[switch.name] = switch
+ return switch_to_eids, switch_name_mapping, skipped
+ def _correct_for_multi_light_intercept(
+ self,
+ entity_ids,
+ switch_to_eids,
+ switch_name_mapping,
+ skipped,
+ ):
# Check for `multi_light_intercept: true/false`
mli = [sw._multi_light_intercept for sw in switch_name_mapping.values()]
more_than_one_switch = len(switch_to_eids) > 1
@@ -1868,7 +1825,86 @@ class AdaptiveLightingManager:
)
skipped = entity_ids
switch_to_eids = {}
+ return switch_to_eids, switch_name_mapping, skipped
+ async def _service_interceptor_turn_on_handler(
+ self,
+ call: ServiceCall,
+ service_data: ServiceData,
+ ) -> None:
+ """Intercept `light.turn_on` and `light.toggle` service calls and adapt them.
+
+ It is possible that the calls are made for multiple lights at once,
+ which in turn might be in different switches or no switches at all.
+ If there are lights that are not all in a single switch, we need to
+ make multiple calls to `light.turn_on` with the correct entity IDs.
+ One of these calls can be intercepted and adapted, the others need to
+ be adapted by calling `_adapt_light` with the correct entity IDs or
+ by calling `light.turn_on` directly.
+
+ We create a mapping from switch to entity IDs and keep a list
+ of skipped lights which are lights in no switches or in switches that
+ are off or lights that are already on.
+
+ If there is only one switch and 0 skipped lights, we just intercept the
+ call directly.
+
+ If there are multiple switches and skipped lights, we can adapt the call
+ for one of the switches to include only the lights in that switch and
+ need to call `_adapt_light` for the other switches with their
+ entity_ids. For skipped lights, we call light.turn_on directly with the
+ entity_ids and original service data.
+
+ If there are only skipped lights, we can use the intercepted call
+ directly.
+ """
+ is_skipped_hash = is_our_context(call.context, "skipped")
+ _LOGGER.debug(
+ "(0) _service_interceptor_turn_on_handler: call.context.id='%s', is_skipped_hash='%s'",
+ call.context.id,
+ is_skipped_hash,
+ )
+ if is_our_context(call.context) and not is_skipped_hash:
+ # Don't adapt our own service calls, but do re-adapt calls that
+ # were skipped by us
+ return
+
+ if (
+ ATTR_EFFECT in service_data[CONF_PARAMS]
+ or ATTR_FLASH in service_data[CONF_PARAMS]
+ ):
+ return
+
+ _LOGGER.debug(
+ "(1) _service_interceptor_turn_on_handler: call='%s', service_data='%s'",
+ call,
+ service_data,
+ )
+
+ # Because `_service_interceptor_turn_on_single_light_handler` modifies the
+ # original service data, we need to make a copy of it to use in the `skipped` call
+ service_data_copy = deepcopy(service_data)
+
+ entity_ids = self._get_entity_list(service_data)
+ # Note: we do not expand light groups anywhere in this method, instead
+ # we skip them and rely on the followup call that HA will make
+ # with the expanded entity IDs.
+
+ switch_to_eids, switch_name_mapping, skipped = self._separate_entity_ids(
+ entity_ids,
+ service_data,
+ )
+
+ (
+ switch_to_eids,
+ switch_name_mapping,
+ skipped,
+ ) = self._correct_for_multi_light_intercept(
+ entity_ids,
+ switch_to_eids,
+ switch_name_mapping,
+ skipped,
+ )
_LOGGER.debug(
"(2) _service_interceptor_turn_on_handler: switch_to_eids='%s', skipped='%s'",
switch_to_eids,
@@ -1886,7 +1922,7 @@ class AdaptiveLightingManager:
has_intercepted = False # Can only intercept a turn_on call once
for adaptive_switch_name, _entity_ids in switch_to_eids.items():
switch = switch_name_mapping[adaptive_switch_name]
- transition = data[CONF_PARAMS].get(
+ transition = service_data[CONF_PARAMS].get(
ATTR_TRANSITION,
switch.initial_transition,
)
@@ -1900,7 +1936,7 @@ class AdaptiveLightingManager:
switch=switch,
transition=transition,
call=call,
- data=modify_service_data(data, _entity_ids),
+ data=modify_service_data(service_data, _entity_ids),
)
has_intercepted = True
continue
@@ -1930,19 +1966,12 @@ class AdaptiveLightingManager:
# Call light turn_on service for skipped entities
context = switch.create_context("skipped")
_LOGGER.debug(
- "(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'",
+ "(5) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', service_data: '%s', context='%s'",
skipped,
- data,
+ service_data_copy, # This is the original service data
context.id,
)
- # Need to expand light groups here because otherwise this interceptor loop will happen twice more
- _LOGGER.debug(
- "(6) _service_interceptor_turn_on_handler: calling `light.turn_on` with skipped='%s', data: '%s', context='%s'",
- skipped,
- data,
- context.id,
- )
- service_data = {ATTR_ENTITY_ID: skipped, **data[CONF_PARAMS]}
+ service_data = {ATTR_ENTITY_ID: skipped, **service_data_copy[CONF_PARAMS]}
if (
ATTR_COLOR_TEMP in service_data
and ATTR_COLOR_TEMP_KELVIN in service_data
From 0e23e906dadd44127b2eebc51d1fd632549f4986 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 15 Aug 2023 14:47:43 -0700
Subject: [PATCH 126/549] Fix #745, first time YAML setup (#752)
* Check whether this is the first time setup
* Keep list
* simplify
* no UI tracking
---
custom_components/adaptive_lighting/config_flow.py | 8 +++++---
custom_components/adaptive_lighting/switch.py | 6 ++----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/custom_components/adaptive_lighting/config_flow.py b/custom_components/adaptive_lighting/config_flow.py
index 3dd89fd4..8f82582a 100644
--- a/custom_components/adaptive_lighting/config_flow.py
+++ b/custom_components/adaptive_lighting/config_flow.py
@@ -42,13 +42,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_import(self, user_input=None):
"""Handle configuration by YAML file."""
await self.async_set_unique_id(user_input[CONF_NAME])
+ # Keep a list of switches that are configured via YAML
+ data = self.hass.data.setdefault(DOMAIN, {})
+ data.setdefault("__yaml__", set()).add(self.unique_id)
+
for entry in self._async_current_entries():
if entry.unique_id == self.unique_id:
- # Keep a list of switches that are configured via YAML
- data = self.hass.data.setdefault(DOMAIN, {})
- data.setdefault("__yaml__", []).append(self.unique_id)
self.hass.config_entries.async_update_entry(entry, data=user_input)
self._abort_if_unique_id_configured()
+
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
@staticmethod
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 5f69dd5f..ff951d15 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -410,9 +410,9 @@ async def async_setup_entry( # noqa: PLR0915
data,
config_entry,
)
- if ( # Skip deleted YAML config entries
+ if ( # Skip deleted YAML config entries or first time YAML config entries
config_entry.source == SOURCE_IMPORT
- and config_entry.unique_id not in data.get("__yaml__", [])
+ and config_entry.unique_id not in data.get("__yaml__", set())
):
_LOGGER.warning(
"Deleting AdaptiveLighting switch '%s' because YAML"
@@ -1680,8 +1680,6 @@ class AdaptiveLightingManager:
self._service_interceptor_turn_on_handler,
),
)
-
- _LOGGER.debug("Proactive adaptation enabled")
except RuntimeError:
_LOGGER.warning(
"Failed to set up service call interceptors, "
From 7a7baafcb32a453588b95a0897f332eec2e185a1 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 15 Aug 2023 15:25:19 -0700
Subject: [PATCH 127/549] [pre-commit.ci] pre-commit autoupdate (#748)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.282 → v0.0.284](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.282...v0.0.284)
* Ruff fixes
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt
---
.pre-commit-config.yaml | 2 +-
custom_components/adaptive_lighting/hass_utils.py | 7 +++----
custom_components/adaptive_lighting/switch.py | 11 +++++------
3 files changed, 9 insertions(+), 11 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 31fdf86c..e7b8b253 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,7 +8,7 @@ repos:
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.0.282
+ rev: v0.0.284
hooks:
- id: ruff
args: ["--fix"]
diff --git a/custom_components/adaptive_lighting/hass_utils.py b/custom_components/adaptive_lighting/hass_utils.py
index ba5bb8b0..cc3257ce 100644
--- a/custom_components/adaptive_lighting/hass_utils.py
+++ b/custom_components/adaptive_lighting/hass_utils.py
@@ -51,12 +51,11 @@ def setup_service_call_interceptor(
# Convert data back to read-only
call.data = ReadOnlyDict(data)
- except Exception as e: # noqa: BLE001
+ except Exception:
# Blindly catch all exceptions to avoid breaking light.turn_on
- _LOGGER.error(
- "Error for call '%s' in service_func_proxy: '%s'",
+ _LOGGER.exception(
+ "Error for call '%s' in service_func_proxy",
call.data,
- e,
)
# Call original service handler with processed data
await existing_service.job.target(call)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index ff951d15..7e910525 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -681,7 +681,7 @@ def _convert_attributes(attributes: dict[str, Any]) -> dict[str, Any]:
if rgb is not None:
attributes[ATTR_RGB_COLOR] = rgb
- _LOGGER.debug(f"Converted {attributes} to rgb {rgb}")
+ _LOGGER.debug("Converted attributes %s to rgb %s", attributes, rgb)
else:
_LOGGER.debug("No suitable color conversion found for %s", attributes)
@@ -1041,11 +1041,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
try:
# HACK: this is a private method in `Entity` which can change
super()._call_on_remove_callbacks()
- except AttributeError as err:
- _LOGGER.error(
- "%s: Caught AttributeError in `_call_on_remove_callbacks`: %s",
+ except AttributeError:
+ _LOGGER.exception(
+ "%s: Caught AttributeError in `_call_on_remove_callbacks`",
self._name,
- err,
)
def _remove_interval_listener(self) -> None:
@@ -1816,7 +1815,7 @@ class AdaptiveLightingManager:
single_switch_with_multiple_lights and switch_without_multi_light_intercept
):
_LOGGER.warning(
- "Single switch with multiple lights targeted, but"
+ "Single switch with multiple lights targeted (%s), but"
" `multi_light_intercept: true` is not set, so skipping intercept"
" for all lights.",
switch_to_eids,
From 792bed8cf6682056c5d5b651d1557f307e6102c9 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 15 Aug 2023 15:25:29 -0700
Subject: [PATCH 128/549] Bump to 1.19.1 in manifest.json
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index eb7f0155..e93d3897 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.19.0"
+ "version": "1.19.1"
}
From 7e84ed0a8433d39b339afa4762392294c3a50886 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 15 Aug 2023 17:09:18 -0700
Subject: [PATCH 129/549] Use WebLate for translations (#753)
* Add license
* lunk
* chore(docs): update TOC
* fix
---------
Co-authored-by: basnijholt
---
README.md | 12 +++++
.../adaptive_lighting/translations/LICENSE.md | 44 +++++++++++++++++++
2 files changed, 56 insertions(+)
create mode 100644 custom_components/adaptive_lighting/translations/LICENSE.md
diff --git a/README.md b/README.md
index 1f2065c7..1997c351 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,7 @@ The `adaptive_lighting.manual_control` event is fired when a light is marked as
- [Custom brightness ramps using `brightness_mode` with `"linear"` and `"tanh"`](#custom-brightness-ramps-using-brightness_mode-with-linear-and-tanh)
- [:eyes: See also](#eyes-see-also)
- [:busts_in_silhouette: Contributors](#busts_in_silhouette-contributors)
+- [Translating Adaptive Lighting](#translating-adaptive-lighting)
@@ -540,3 +541,14 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
+
+## Translating Adaptive Lighting
+
+Help to translate Adaptive Lighting into your language on [Hosted Weblate](https://hosted.weblate.org/engage/adaptive-lighting/)!
+
+Translating can be done from your webbrowser, no programming knowledge
+is needed!
+
+
+
+
diff --git a/custom_components/adaptive_lighting/translations/LICENSE.md b/custom_components/adaptive_lighting/translations/LICENSE.md
new file mode 100644
index 00000000..5916442d
--- /dev/null
+++ b/custom_components/adaptive_lighting/translations/LICENSE.md
@@ -0,0 +1,44 @@
+This license **only** applies to all files in `custom_components/adaptive_lighting/translations/` in the Adaptive Lighting repository.
+For these translations we wave copyright and related rights through the CC0 1.0 Universal license.
+
+# Creative Commons CC0 1.0 Universal
+
+CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER.
+
+### Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. **Copyright and Related Rights.** A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
+
+ ii. moral rights retained by the original author(s) and/or performer(s);
+
+ iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
+
+ iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below;
+
+ v. rights protecting the extraction, dissemination, use and reuse of data in a Work;
+
+ vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
+
+ vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
+
+2. **Waiver.** To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
+
+3. **Public License Fallback.** Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
+
+4. **Limitations and Disclaimers.**
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
+
+ b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
+
+ c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
+
+ d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
From 93cfda427a084972252925fc4af154fa0b5e8e6a Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Wed, 16 Aug 2023 04:09:55 +0200
Subject: [PATCH 130/549] Translations update from Hosted Weblate (#754)
* Update translation files
Updated by "Cleanup translation files" hook in Weblate.
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/
* Translated using Weblate (Dutch)
Currently translated at 24.1% (37 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
---------
Co-authored-by: Bas Nijholt
---
.../adaptive_lighting/translations/nl.json | 110 ++++++++++--------
.../adaptive_lighting/translations/sv.json | 91 +++++++--------
2 files changed, 104 insertions(+), 97 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index a0b6b082..cc269685 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -1,57 +1,67 @@
{
- "title": "Adaptieve verlichting",
- "config": {
- "step": {
- "user": {
- "title": "Kies een naam voor de adaptieve verlichting integratie",
- "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!",
- "data": {
- "name": "Naam"
+ "title": "Adaptieve verlichting",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Kies een naam voor de adaptieve verlichting integratie",
+ "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!",
+ "data": {
+ "name": "Naam"
+ }
+ }
+ },
+ "abort": {
+ "already_configured": "Dit apparaat is al geconfigureerd"
}
- }
},
- "abort": {
- "already_configured": "Dit apparaat is al geconfigureerd"
- }
- },
- "options": {
- "step": {
- "init": {
- "title": "Adaptieve verlichting instellingen",
- "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.",
- "data": {
- "lights": "Lichten",
- "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)",
- "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)",
- "interval": "interval: Tijd tussen switch-updates. (seconden)",
- "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)",
- "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)",
- "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)",
- "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)",
- "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.",
- "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.",
- "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).",
- "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.",
- "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'",
- "sleep_rgb_color": "sleep_rgb_color, in RGB",
- "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)",
- "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)",
- "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
- "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
- "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)",
- "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
- "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)",
- "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.",
- "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)",
- "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)",
- "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen."
+ "options": {
+ "step": {
+ "init": {
+ "title": "Adaptieve verlichting instellingen",
+ "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.",
+ "data": {
+ "lights": "Lichten",
+ "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)",
+ "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)",
+ "interval": "interval: Tijd tussen switch-updates. (seconden)",
+ "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)",
+ "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)",
+ "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)",
+ "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)",
+ "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.",
+ "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.",
+ "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).",
+ "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.",
+ "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'",
+ "sleep_rgb_color": "sleep_rgb_color, in RGB",
+ "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)",
+ "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)",
+ "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
+ "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
+ "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)",
+ "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
+ "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)",
+ "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.",
+ "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)",
+ "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)",
+ "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer je de lichten in eerste instantie aanzet. Indien ingesteld op `true`, past AL zich alleen aan als `light.turn_on` wordt aangeroepen zonder kleur of helderheid op te geven. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Indien `false`, past AL zich aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. Moet 'take_over_control' ingeschakeld zijn. 🕵️ "
+ }
+ }
+ },
+ "error": {
+ "option_error": "Ongeldige optie",
+ "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant"
}
- }
},
- "error": {
- "option_error": "Ongeldige optie",
- "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant"
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "only_once": {
+ "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄"
+ }
+ }
+ }
}
- }
}
diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json
index 2239ad3b..fe5fb653 100644
--- a/custom_components/adaptive_lighting/translations/sv.json
+++ b/custom_components/adaptive_lighting/translations/sv.json
@@ -1,53 +1,50 @@
{
- "title": "Adaptiv Ljussättning",
- "config": {
- "step": {
- "user": {
- "title": "Välj ett namn för Adaptiv Ljussättning",
- "description": "Varje konfiguration kan innehålla flera ljuskällor!",
- "data": {
- "name": "Namn"
+ "title": "Adaptiv Ljussättning",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Välj ett namn för Adaptiv Ljussättning",
+ "description": "Varje konfiguration kan innehålla flera ljuskällor!",
+ "data": {
+ "name": "Namn"
+ }
+ }
+ },
+ "abort": {
+ "already_configured": "Enheten är redan konfiguerad"
}
- }
},
- "abort": {
- "already_configured": "Enheten är redan konfiguerad"
- }
- },
- "options": {
- "step": {
- "init": {
- "title": "Adaptiv Ljussättning Inställningar",
- "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.",
- "data": {
- "lights": "lights, ljuskällor",
- "adapt_brightness": "adapt_brightness, Adaptiv ljusstyrka",
- "adapt_color_temp": "adapt_color_temp, Justera färgtemperatur genom att använda 'color_temp' om möjligt",
- "adapt_rgb_color": "adapt_rgb_color, Justera färgtemperatur genom att använda RGB/XY om möjligt",
- "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras",
- "interval": "interval, Tid mellan uppdateringar i sekunder",
- "max_brightness": "max_brightness, i procent %",
- "max_color_temp": "max_color_temp, i Kelvin",
- "min_brightness": "min_brightness, i %",
- "min_color_temp": "min_color_temp, i Kelvin",
- "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'",
- "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt",
- "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.",
- "sleep_brightness": "sleep_brightness, i %",
- "sleep_color_temp": "sleep_color_temp, i Kelvin",
- "sunrise_offset": "sunrise_offset, i +/- sekunder",
- "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)",
- "sunset_offset": "sunset_offset, i +/- sekunder",
- "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)",
- "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen",
- "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)",
- "transition": "transition, i sekunder"
+ "options": {
+ "step": {
+ "init": {
+ "title": "Adaptiv Ljussättning Inställningar",
+ "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.",
+ "data": {
+ "lights": "lights, ljuskällor",
+ "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras",
+ "interval": "interval, Tid mellan uppdateringar i sekunder",
+ "max_brightness": "max_brightness, i procent %",
+ "max_color_temp": "max_color_temp, i Kelvin",
+ "min_brightness": "min_brightness, i %",
+ "min_color_temp": "min_color_temp, i Kelvin",
+ "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'",
+ "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt",
+ "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.",
+ "sleep_brightness": "sleep_brightness, i %",
+ "sleep_color_temp": "sleep_color_temp, i Kelvin",
+ "sunrise_offset": "sunrise_offset, i +/- sekunder",
+ "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)",
+ "sunset_offset": "sunset_offset, i +/- sekunder",
+ "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)",
+ "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen",
+ "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)",
+ "transition": "transition, i sekunder"
+ }
+ }
+ },
+ "error": {
+ "option_error": "Ogiltlig inställning",
+ "entity_missing": "Ett valt ljus hittades inte"
}
- }
- },
- "error": {
- "option_error": "Ogiltlig inställning",
- "entity_missing": "Ett valt ljus hittades inte"
}
- }
}
From 627c7a120c0ea54a9de6d881dd8ec896cb9dc145 Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Wed, 16 Aug 2023 05:02:47 +0200
Subject: [PATCH 131/549] Translations update from Hosted Weblate (de, nl, fr)
(#755)
* Translated using Weblate (German)
Currently translated at 0.0% (0 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/de/
* Translated using Weblate (French)
Currently translated at 0.0% (0 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/
* Translated using Weblate (Dutch)
Currently translated at 56.8% (87 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
---------
Co-authored-by: Bas Nijholt
---
.../adaptive_lighting/translations/de.json | 313 +++++++++++++++---
.../adaptive_lighting/translations/fr.json | 306 ++++++++++++++---
.../adaptive_lighting/translations/nl.json | 6 +
3 files changed, 530 insertions(+), 95 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json
index 79eb0df8..abb12101 100644
--- a/custom_components/adaptive_lighting/translations/de.json
+++ b/custom_components/adaptive_lighting/translations/de.json
@@ -1,58 +1,269 @@
{
- "title": "Adaptive Lighting",
- "config": {
- "step": {
- "user": {
- "title": "Benenne das Adaptive Lighting",
- "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten",
- "data": {
- "name": "Name"
+ "title": "Verwendbare Beleuchtung",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Wähl aus einen Namen für die Verwendbare Beleuchtung Instanz",
+ "description": "Jede Instanz kann mehrfache Lichter zügeln!",
+ "data": {
+ "name": "Name"
+ }
+ }
+ },
+ "abort": {
+ "already_configured": "Diese Vorrichtung ist schon konfiguriert"
}
- }
},
- "abort": {
- "already_configured": "Gerät ist bereits konfiguriert!"
- }
- },
- "options": {
- "step": {
- "init": {
- "title": "Adaptive Lighting Optionen",
- "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.",
- "data": {
- "lights": "Lichter",
- "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt",
- "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)",
- "interval": "interval, Zeit zwischen Updates des Switches",
- "max_brightness": "max_brightness, maximale Helligkeit in %",
- "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin",
- "min_brightness": "min_brightness, minimale Helligkeit in %",
- "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin",
- "only_once": "only_once, passe die Lichter nur beim Einschalten an",
- "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich",
- "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.",
- "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.",
- "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'",
- "sleep_rgb_color": "sleep_rgb_color, in RGB",
- "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin",
- "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden",
- "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)",
- "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)",
- "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden",
- "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)",
- "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)",
- "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.",
- "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)",
- "transition": "transition, Wechselzeit in Sekunden",
- "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.",
- "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben."
+ "options": {
+ "step": {
+ "init": {
+ "title": "Verwendbare Beleuchtung Optionen",
+ "description": "Konfigurier eine Verwendbare Beleuchtung Komponente. Option Namen stimmen ab mit die YAML Lagen. Ob du hast diesen Eintrag definiert herein YAML, keine Optionen wollen fungieren hier. Für interaktiv Graphen #der demonstrieren Parameter Effekte, besuchen [dieses Web #App](https://basnijholt.github.io/Verwendbar-Beleuchtung). Für ferner Details, seht die [offizielle Dokumentation](https://github.com/basnijholt/Verwendbar-Beleuchtung#readme).",
+ "data": {
+ "lights": "Lichter: Liste von licht Entität_ids zu sein reguliert (darf sein leer). 🌟",
+ "initial_transition": "Einleitender_Wechsel",
+ "sleep_transition": "Schlaf_Wechsel",
+ "interval": "Intervall",
+ "max_brightness": "max_Helligkeit: #Höchster Helligkeit Prozentsatz. 💡",
+ "max_color_temp": "max_Farbe_Aushilfskraft: #Kalt Farbe Temperatur in #Kelvin. ❄️",
+ "min_brightness": "min_Helligkeit: Minimaler Helligkeit Prozentsatz. 💡",
+ "min_color_temp": "min_Farbe_Aushilfskraft: #Warm Farbe Temperatur in #Kelvin. 🔥",
+ "only_once": "Nur_einmal: Adaptier nur Lichter als sind angemacht sie (`wahr`) oder Unterhalt adaptieren ihnen (`falsch`). 🔄",
+ "prefer_rgb_color": "Bevorzug_rgb_Farbe: Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈",
+ "separate_turn_on_commands": "Getrennte_Drehung_auf_Befehle: Nutzung trennen `Licht.Drehung_holt ab` weiter Farbe und Helligkeit, gebraucht für einige Licht Typen. 🔀",
+ "send_split_delay": "#Senden_geteilt_Aufschub",
+ "sleep_brightness": "Schlaf_Helligkeit",
+ "sleep_rgb_or_color_temp": "Schlaf_rgb_oder_Farbe_Aushilfskraft",
+ "sleep_rgb_color": "Schlaf_rgb_Farbe",
+ "sleep_color_temp": "Schlaf_Farbe_Aushilfskraft",
+ "sunrise_offset": "Sonnenaufgang_Verschiebung",
+ "sunrise_time": "Sonnenaufgang_Zeit",
+ "max_sunrise_time": "max_Sonnenaufgang_Zeit",
+ "sunset_offset": "Abendrot_Verschiebung",
+ "sunset_time": "Abendrot_Zeit",
+ "min_sunset_time": "min_Abendrot_Zeit",
+ "take_over_control": "Nimm_über_Aufsicht: Schalte aus Verwendbare Beleuchtung ob anderen Quelle Anrufe `hell.Drehung_auf` während sind Lichter weiter und sein adaptiert. Note dass diese Anrufe `homeassistant.Update_Entität` jedes `Intervall`! 🔒",
+ "detect_non_ha_changes": "Finde heraus_nicht_ha_Änderungen: Findet heraus und hält an Bearbeitungen für nicht-`Licht.Drehung_auf` staatlich Änderungen. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ Vorsicht: ⚠️ Einige Lichter dürfen hindeuten #kein 'weiter' Staat, #welche konnte resultieren in Lichter anmachen unerwartet. Schalte aus diesen Charakterzug ob du begegnest solche Sachverhalte.",
+ "transition": "Wechsel",
+ "adapt_delay": "Adaptier_Aufschub",
+ "skip_redundant_commands": "Sprung_redundant_Befehle: Sprung senden Bearbeitung gebietet wessen anrichten schon staatlich #angleichen das Lichts gekannt Staat. Minimiert Netzwerk Verkehr und verbessert die Bearbeitung responsivity in einigen Situationen. 📉Schalte aus ob physische Licht Staaten steigen aus von synchronisieren mit HAaufgezeichnet haben Staat.",
+ "autoreset_control_seconds": "autoreset_Aufsicht_Sekunden",
+ "brightness_mode": "Helligkeit_Verfahren",
+ "brightness_mode_time_dark": "Helligkeit_Verfahren_Zeit_Finsternis",
+ "brightness_mode_time_light": "Helligkeit_Verfahren_Zeit_Licht",
+ "max_sunset_time": "max_Abendrot_Zeit",
+ "transition_until_sleep": "Wechsel_bis_Schlaf: Wann aktiviert, Verwendbare Beleuchtung will spendieren schlafen Lagen da das Minimum, übergehen zu diesen Werten nach Abendrot. 🌙",
+ "min_sunrise_time": "min_Sonnenaufgang_Zeit",
+ "adapt_only_on_bare_turn_on": "Adaptier_nur_auf_kahl_Drehung_weiter: Wann drehen weiter Lichter anfänglich. Ob gesetzt zu `wahr`, #AL adaptiert nur ob `Licht.Drehung_ist aufgerufen` weiter ohne präzisieren Farbe oder Helligkeit. ❌🌈 Dies #z.B., verhindert als Bearbeitung aktivierend eine Szene. Ob `falsch`, #AL adaptiert #ohne Rücksicht auf von dem Vorliegen von Farbe oder Helligkeit in der einleitenden `Bedienung_Daten`. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ ",
+ "intercept": "Unterbrich: Unterbrich und adaptier `Licht.Drehung_auf` Anrufe zu aktivieren momentane Farbe und Helligkeit Bearbeitung. 🏎️ Schalte aus für Lichter #der unterstützen nicht `Licht.Drehung_weiter` mit Farbe und Helligkeit.",
+ "multi_light_intercept": "multi_Licht_unterbricht: Unterbrich und adaptier `Licht.Drehung_auf` Anrufe jenes Soll mehrfache Lichter. ➗⚠️ Dies darf resultieren in #aufspalten ein lediges `Licht.Drehung_auf` Anruf hinein mehrfach Anrufe, #z.B., als sind Lichter in verschieden Schalter. Bedürft `unterbrechen` zu sein aktiviert.",
+ "include_config_in_attributes": "Schließ ein_config_in_Attribute: Vorstellung jede Optionen da Attribute auf dem Schalter in Heim Stellvertretend als gesetzt zu `wahr`. 📝"
+ },
+ "data_description": {
+ "initial_transition": "Dauer von dem ersten Wechsel zündet an als Drehung von `ab` zu `weiter` in Sekunden. ⏲️",
+ "sleep_brightness": "Helligkeit Prozentsatz von Lichter in schlafen Verfahren. 😴",
+ "sleep_rgb_color": "#RGB Farbe in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist \"rgb_Farbe\"). 🌈",
+ "sleep_transition": "Dauer von Wechsel schläft \"als Verfahren\" ist #festknebeln in Sekunden. 😴",
+ "sunrise_time": "Gesetzt eine feste Zeit (HH:MM:SS) Für Sonnenaufgang. 🌅",
+ "min_sunrise_time": "Setzte den frühesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für später Sonnenaufgänge. 🌅",
+ "interval": "Häufigkeit zu adaptieren die Lichter, in Sekunden. 🔄",
+ "transition": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑",
+ "sleep_rgb_or_color_temp": "Nutzung entweder `\"rgb_Farbe\"` oder `\"Farbe_Aushilfskraft\"` in Schlaf Verfahren. 🌙",
+ "sleep_color_temp": "Farbe Temperatur in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist `Farbe_Aushilfskraft`) in #Kelvin. 😴",
+ "max_sunrise_time": "Setzte den spätesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für früher Sonnenaufgänge. 🌅",
+ "sunrise_offset": "Pass an Sonnenaufgang Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰",
+ "sunset_time": "Gesetzt eine feste Zeit (HH:MM:SS) Für Abendrot. 🌇",
+ "min_sunset_time": "Setzte das früheste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für später Abendrot. 🌇",
+ "max_sunset_time": "Setzte das späteste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für früher Abendrot. 🌇",
+ "sunset_offset": "Pass an Abendrot Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰",
+ "brightness_mode": "Helligkeit Verfahren zu benutzen. Mögliche Werte sind `#voreingestellt`, `geradlinig`, und `tanh` (Nutzungen `Helligkeit_Verfahren_Zeit_Finsternis` und `Helligkeit_Verfahren_Zeit_hell`). 📈",
+ "brightness_mode_time_dark": "(Überhört ob `Helligkeit_Verfahren='#voreingestellt'`) Die Dauer in Sekunden zu #Nepp oben/#vor herunter die Helligkeit/nach Sonnenaufgang/Abendrot. 📈📉",
+ "brightness_mode_time_light": "(Überhört ob `Helligkeit_Verfahren='#voreingestellt'`) Die Dauer in Sekunden zu #Nepp oben/#nachdem herunter die Helligkeit/vor Sonnenaufgang/Abendrot. 📈📉.",
+ "autoreset_control_seconds": "Automatisch #rücksetzen die manuelle Aufsicht nach einer Nummer von Sekunden. Apparat zu 0 zu ausschalten. ⏲️",
+ "send_split_delay": "Aufschub (ms) zwischen `getrennt_Drehung_auf_Befehle` für Lichter #der unterstützen nicht zeitgleiche Helligkeit und Farbe Lage. ⏲️",
+ "adapt_delay": "Wartezeit Zeit (Sekunden) zwischen Licht macht an und Verwendbare Beleuchtung bewerbend Änderungen. Dürfen helfen zu vermeiden flackern. ⏲️"
+ }
+ }
+ },
+ "error": {
+ "option_error": "Kränkliche Option",
+ "entity_missing": "#Man oder #mehr ausgewählt Licht Entitäten fehlen von Heim Assistenten"
}
- }
},
- "error": {
- "option_error": "Fehlerhafte Option",
- "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden"
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "use_defaults": {
+ "description": "Setzt nicht die #voreingestellt Werte präzisiert in diesen Bedienung Anruf. Optionen: \"Lauf\" (#voreingestellt, hält fest gängige Werte), \"Fabrik\" (#Nachstellung zu dokumentieren Vorgaben), oder \"Konfiguration\" (fällt zurück zu schalten config Vorgaben). ⚙️",
+ "name": "Benutzen_Vorgaben"
+ },
+ "include_config_in_attributes": {
+ "description": "Vorstellung jede Optionen da Attribute auf dem Schalter in Heim Stellvertretend als gesetzt zu `wahr`. 📝",
+ "name": "Schließ ein_config_in_Attribute"
+ },
+ "turn_on_lights": {
+ "description": "Ob zu anmachen Lichter jener ist zurzeit ab. 🔆",
+ "name": "Drehung_auf_Lichter"
+ },
+ "initial_transition": {
+ "description": "Dauer von dem ersten Wechsel zündet an als Drehung von `ab` zu `weiter` in Sekunden. ⏲️",
+ "name": "Einleitender_Wechsel"
+ },
+ "sleep_transition": {
+ "description": "Dauer von Wechsel schläft \"als Verfahren\" ist #festknebeln in Sekunden. 😴",
+ "name": "Schlaf_Wechsel"
+ },
+ "max_brightness": {
+ "description": "#Höchster Helligkeit Prozentsatz. 💡",
+ "name": "max_Helligkeit"
+ },
+ "max_color_temp": {
+ "description": "#Kalt Farbe Temperatur in #Kelvin. ❄️",
+ "name": "max_Farbe_Aushilfskraft"
+ },
+ "min_brightness": {
+ "description": "Minimaler Helligkeit Prozentsatz. 💡",
+ "name": "min_Helligkeit"
+ },
+ "only_once": {
+ "description": "Adaptier nur Lichter als sind angemacht sie (`wahr`) oder Unterhalt adaptieren ihnen (`falsch`). 🔄",
+ "name": "Nur_einmal"
+ },
+ "prefer_rgb_color": {
+ "description": "Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈",
+ "name": "Bevorzug_rgb_Farbe"
+ },
+ "send_split_delay": {
+ "description": "Aufschub (ms) zwischen `getrennt_Drehung_auf_Befehle` für Lichter #der unterstützen nicht zeitgleiche Helligkeit und Farbe Lage. ⏲️",
+ "name": "#Senden_geteilt_Aufschub"
+ },
+ "sleep_brightness": {
+ "description": "Helligkeit Prozentsatz von Lichter in schlafen Verfahren. 😴",
+ "name": "Schlaf_Helligkeit"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "Nutzung entweder `\"rgb_Farbe\"` oder `\"Farbe_Aushilfskraft\"` in Schlaf Verfahren. 🌙",
+ "name": "Schlaf_rgb_oder_Farbe_Aushilfskraft"
+ },
+ "sleep_rgb_color": {
+ "description": "#RGB Farbe in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist \"rgb_Farbe\"). 🌈",
+ "name": "Schlaf_rgb_Farbe"
+ },
+ "sleep_color_temp": {
+ "description": "Farbe Temperatur in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist `Farbe_Aushilfskraft`) in #Kelvin. 😴",
+ "name": "Schlaf_Farbe_Aushilfskraft"
+ },
+ "sunrise_offset": {
+ "description": "Pass an Sonnenaufgang Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰",
+ "name": "Sonnenaufgang_Verschiebung"
+ },
+ "sunrise_time": {
+ "description": "Gesetzt eine feste Zeit (HH:MM:SS) Für Sonnenaufgang. 🌅",
+ "name": "Sonnenaufgang_Zeit"
+ },
+ "sunset_offset": {
+ "description": "Pass an Abendrot Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰",
+ "name": "Abendrot_Verschiebung"
+ },
+ "take_over_control": {
+ "description": "Schalte aus Verwendbare Beleuchtung ob anderen Quelle Anrufe `hell.Drehung_auf` während sind Lichter weiter und sein adaptiert. Note dass diese Anrufe `homeassistant.Update_Entität` jedes `Intervall`! 🔒",
+ "name": "Nimm_über_Aufsicht"
+ },
+ "detect_non_ha_changes": {
+ "description": "Findet heraus und hält an Bearbeitungen für nicht-`Licht.Drehung_auf` staatlich Änderungen. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ Vorsicht: ⚠️ Einige Lichter dürfen hindeuten #kein 'weiter' Staat, #welche konnte resultieren in Lichter anmachen unerwartet. Schalte aus diesen Charakterzug ob du begegnest solche Sachverhalte.",
+ "name": "Finde heraus_nicht_ha_Änderungen"
+ },
+ "transition": {
+ "description": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑",
+ "name": "Wechsel"
+ },
+ "adapt_delay": {
+ "description": "Wartezeit Zeit (Sekunden) zwischen Licht macht an und Verwendbare Beleuchtung bewerbend Änderungen. Dürfen helfen zu vermeiden flackern. ⏲️",
+ "name": "Adaptier_Aufschub"
+ },
+ "autoreset_control_seconds": {
+ "description": "Automatisch #rücksetzen die manuelle Aufsicht nach einer Nummer von Sekunden. Apparat zu 0 zu ausschalten. ⏲️",
+ "name": "autoreset_Aufsicht_Sekunden"
+ },
+ "min_color_temp": {
+ "description": "#Warm Farbe Temperatur in #Kelvin. 🔥",
+ "name": "min_Farbe_Aushilfskraft"
+ },
+ "separate_turn_on_commands": {
+ "description": "Nutzung trennen `Licht.Drehung_holt ab` weiter Farbe und Helligkeit, gebraucht für einige Licht Typen. 🔀",
+ "name": "Getrennte_Drehung_auf_Befehle"
+ },
+ "sunset_time": {
+ "description": "Gesetzt eine feste Zeit (HH:MM:SS) Für Abendrot. 🌇",
+ "name": "Abendrot_Zeit"
+ },
+ "max_sunrise_time": {
+ "description": "Setzte den spätesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für früher Sonnenaufgänge. 🌅",
+ "name": "max_Sonnenaufgang_Zeit"
+ },
+ "min_sunset_time": {
+ "description": "Setzte das früheste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für später Abendrot. 🌇",
+ "name": "min_Abendrot_Zeit"
+ },
+ "entity_id": {
+ "name": "Entität_id",
+ "description": "Entität ID von dem Schalter. 📝"
+ }
+ },
+ "name": "Änderung_Schalter_Lagen",
+ "description": "Änderung irgendwelche Lagen wolltest du mögen in dem Schalter. Alle Optionen sind hier #dieselbe wie herein die config Strömung."
+ },
+ "set_manual_control": {
+ "fields": {
+ "manual_control": {
+ "name": "Manuelle_Aufsicht",
+ "description": "Ob zu zufügen (\"wahr\") oder entfernen (\"falsch\") das Licht von der \"manuellen_Aufsicht\" Liste. 🔒"
+ },
+ "lights": {
+ "name": "Lichter",
+ "description": "Entität_id(s) von Lichter, ob nicht präzisiert, alle Lichter in dem Schalter ist ausgewählt. 💡"
+ },
+ "entity_id": {
+ "description": "Die `Entität_id` von dem Schalter in #welche zu (un)zensiert das lichtes da sein `manuell reguliert`. 📝",
+ "name": "Entität_id"
+ }
+ },
+ "name": "Gesetzt_manuelle_Aufsicht",
+ "description": "Mark ob ist ein Licht 'manuell reguliert'."
+ },
+ "apply": {
+ "fields": {
+ "prefer_rgb_color": {
+ "name": "Bevorzug_rgb_Farbe",
+ "description": "Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈"
+ },
+ "transition": {
+ "name": "Wechsel",
+ "description": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑"
+ },
+ "adapt_brightness": {
+ "name": "Adaptier_Helligkeit",
+ "description": "Ob zu adaptieren die Helligkeit von dem Licht. 🌞"
+ },
+ "entity_id": {
+ "name": "Entität_id",
+ "description": "Die `Entität_id` von dem Schalter mit den Lagen zu bewerben. 📝"
+ },
+ "lights": {
+ "name": "Lichter",
+ "description": "Ein lichtes (oder Liste von Lichter) zu bewerben die Lagen zu. 💡"
+ },
+ "turn_on_lights": {
+ "name": "Drehung_auf_Lichter",
+ "description": "Ob zu anmachen Lichter jener ist zurzeit ab. 🔆"
+ },
+ "adapt_color": {
+ "description": "Ob zu adaptieren die Farbe auf #stützend Lichter. 🌈",
+ "name": "Adaptier_Farbe"
+ }
+ },
+ "name": "Bewirb",
+ "description": "Bewirbt den Lauf Verwendbar Beleuchtung Lagen zu Lichter."
+ }
}
- }
}
diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json
index a41d84a0..6e4c7a8f 100644
--- a/custom_components/adaptive_lighting/translations/fr.json
+++ b/custom_components/adaptive_lighting/translations/fr.json
@@ -1,51 +1,269 @@
{
- "title": "Éclairage adaptatif",
- "config": {
- "step": {
- "user": {
- "title": "Choisissez un nom pour cette instance d'éclairage adaptatif",
- "description": "Choisissez un nom pour cette instance. Vous pouvez configurer plusieurs instances d'éclairage adaptatif, chacune pouvant contrôler plusieurs lampes !",
- "data": {
- "name": "Nom"
+ "title": "Éclairage adaptatif",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Choisissez un nom pour l'instance Adaptive Lighting",
+ "description": "Chaque instance peut contenir plusieurs lumières !",
+ "data": {
+ "name": "Nom"
+ }
+ }
+ },
+ "abort": {
+ "already_configured": "Cet appareil est déjà configuré"
}
- }
},
- "abort": {
- "already_configured": "Cet appareil est déjà configuré"
- }
- },
- "options": {
- "step": {
- "init": {
- "title": "Options d'éclairage adaptatif",
- "description": "Tous les paramètres de l'instance d'éclairage adaptatif. Les noms des options correspondent aux paramètres YAML. Aucune option n'est affichée si l'entrée adaptive_lighting est définie dans votre configuration YAML.",
- "data": {
- "lights": "lights : Les lampes à contrôler",
- "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».",
- "sleep_transition": "sleep_transition : Transition (en secondes) lorsque « sleep_state » est commuté.",
- "interval": "interval : Temps (en secondes) entre deux mises à jour du commutateur.",
- "max_brightness": "max_brightness : Luminosité maximale des lampes (en pourcentage) au cours d'un cycle.",
- "max_color_temp": "max_color_temp : Couleur la plus froide (en kelvins) du cycle de température de couleur.",
- "min_brightness": "min_brightness : Luminosité minimale des lampes (en pourcentage) au cours d'un cycle.",
- "min_color_temp": "min_color_temp : Couleur la plus chaude (en kelvins) du cycle de température de couleur.",
- "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées.",
- "prefer_rgb_color": "prefer_rgb_color : Utiliser « rgb_color » plutôt que « color_temp » lorsque cela est possible.",
- "separate_turn_on_commands": "separate_turn_on_commands : Séparer les commandes pour chaque attribut (couleur, luminosité, etc.) de « light.turn_on » (nécessaire pour certaines lampes).",
- "sleep_brightness": "sleep_brightness : Luminosité (en pourcentage) du mode nuit.",
- "sleep_color_temp": "sleep_color_temp : Température de couleur (en kelvins) du mode nuit.",
- "sunrise_offset": "sunrise_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au lever du soleil.",
- "sunrise_time": "sunrise_time : Heure (HH:MM:SS) du lever du soleil. Si « None », utilise l'heure correspondant à votre emplacement.",
- "sunset_offset": "sunset_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au coucher du soleil.",
- "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.",
- "take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.",
- "detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)",
- "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes."
+ "options": {
+ "step": {
+ "init": {
+ "title": "Options d ' éclairage adaptatifs",
+ "description": "Configurer un adaptatif Élément d'éclairage. Les noms d'options correspondent aux réglages YAML. Si vous avez défini cette entrée dans YAML, aucune option n'apparaîtra ici. Pour les graphiques interactifs qui montrent les effets des paramètres, visitez [cette application Web](https://basnijholt.github.io/adaptive-lighting). Pour plus de détails, voir la [document officiel](https://github.com/basnijholt/adaptive-lighting#readme).",
+ "data": {
+ "lights": "lumières: List of light entity_ids to be controlled (may be empty). 🌟",
+ "initial_transition": "initial_transition",
+ "sleep_transition": "sleep_transition",
+ "interval": "intervalle",
+ "max_brightness": "max_brightness: pourcentage de luminosité maximum. personnalisation",
+ "max_color_temp": "max_color_temp: Température de couleur la plus froide en Kelvin. assemblage",
+ "min_brightness": "min_brightness: Pourcentage de luminosité minimum. personnalisation",
+ "min_color_temp": "min_color_temp: Température de couleur la plus chaude de Kelvin. 🔥",
+ "only_once": "seulement_une fois: Adaptez les lumières seulement lorsqu'elles sont allumées ( \" vrai \" ) ou continuez à les adapter ( \" faux \" ). 🔄",
+ "prefer_rgb_color": "prefer_rgb_color: Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible.",
+ "separate_turn_on_commands": "separate_turn_on_commands: Utilisez des appels séparés `light.turn_on` pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀",
+ "sleep_brightness": "sleep_brightness",
+ "sleep_color_temp": "sleep_color_temp",
+ "sunrise_offset": "sunrise_offset",
+ "sunrise_time": "sunrise_time",
+ "sunset_offset": "coucher de soleil_offset",
+ "sunset_time": "coucher de soleil_time",
+ "take_over_control": "take_over_control: Éclairage adaptatif désactive si une autre source appelle `light.turn_on` alors que les lumières sont allumées et adaptées. Notez que cela appelle `homeassistant.update_entity` chaque `intervale`",
+ "detect_non_ha_changes": "detect_non_ha_changes: Détecte et arrête les adaptations pour non-lumière. changement d'état. Besoins `take_over_control` activé. Certaines lumières pourraient faussement indiquer un état « sur », ce qui pourrait donner lieu à des lumières s'allumer de façon inattendue. Désactivez cette fonctionnalité si vous rencontrez de tels problèmes.",
+ "transition": "transition",
+ "send_split_delay": "send_split_delay",
+ "brightness_mode": "luminosité_mode",
+ "brightness_mode_time_dark": "brightness_mode_time_dark",
+ "brightness_mode_time_light": "luminosité_mode_time_light",
+ "max_sunset_time": "max_sunset_time",
+ "min_sunset_time": "min_sunset_time",
+ "sleep_rgb_color": "sleep_rgb_color",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp",
+ "transition_until_sleep": "transition_until_sleep: Lorsque cela est activé, l'éclairage adaptatif traitera les paramètres de sommeil comme le minimum, en passant à ces valeurs après le coucher du soleil. 🌙",
+ "min_sunrise_time": "min_sunrise_time",
+ "max_sunrise_time": "max_sunrise_time",
+ "autoreset_control_seconds": "autoreset_control_seconds",
+ "adapt_delay": "adapt_delay",
+ "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimise le trafic réseau et améliore la réceptivité de l'adaptation dans certaines situations. 📉Disponible si les états de lumière physique sortent de synchronisation avec l'état enregistré de HA.",
+ "intercept": "intercept: Intercepter et adapter les appels `light.turn_on` pour permettre une adaptation instantanée de la couleur et de la luminosité. ACIA Désactiver les lumières qui ne supportent pas `light.turn_on` avec couleur et luminosité.",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quand on allume les lumières au départ. Si le paramètre est < < vrai > > , AL s ' adapte uniquement si l ' on invoque < < light.turn_on > > sans préciser la couleur ou la luminosité. ❌ Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si `false`, AL s'adapte indépendamment de la présence de couleur ou de luminosité dans le `service_data' initial. Besoins `take_over_control` activé. 🕵∫ ",
+ "multi_light_intercept": "multi_light_intercept: Interceptez et adaptez `light.turn_on` les appels qui ciblent plusieurs lumières. ➗gène Cela pourrait permettre de diviser un seul appel `light.turn_on` en plusieurs appels, par exemple lorsque les lumières sont dans différents interrupteurs. Nécessite un < < intercept > > pour être activé.",
+ "include_config_in_attributes": "include_config_in_attributes: Afficher toutes les options en tant qu'attributs sur le commutateur dans l'assistant d'accueil lorsqu'il s'agit de «true»"
+ },
+ "data_description": {
+ "sleep_rgb_color": "Couleur RGB en mode sommeil (utilisé lorsque `sleep_rgb_or_color_temp` est \"rgb_color\").",
+ "sleep_transition": "Durée de la transition lorsque \"mode de repos\" est activé en quelques secondes. 😴",
+ "sunrise_time": "Réglez un temps fixe (HH:MM:SS) pour le lever du soleil. 🌅",
+ "min_sunrise_time": "Définir le premier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil ultérieures. 🌅",
+ "max_sunrise_time": "Définir le dernier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil antérieures. 🌅",
+ "sunrise_offset": "Réglez le lever de soleil avec un décalage positif ou négatif en quelques secondes. ⏰",
+ "sunset_time": "Réglez un temps fixe (HH:MM:SS) pour le coucher de soleil. 🌇",
+ "min_sunset_time": "Réglez le premier temps de coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇",
+ "max_sunset_time": "Définir le dernier coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil antérieurs. 🌇",
+ "sunset_offset": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰",
+ "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont < < par défaut > > , < < linéaire > > et < < parois > > , < < par défaut > > , et < < par coup > > , < < par défaut > > et par > par > . 📈",
+ "brightness_mode_time_light": "(Ignoré si `brightness_mode='default'`) La durée en quelques secondes pour augmenter/verser la luminosité après/avant le lever du soleil/sunset. 📈📉.",
+ "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫",
+ "send_split_delay": "Délai (ms) entre `separate_turn_on_commands` pour les lumières qui ne supportent pas la luminosité simultanée et le réglage de couleur. ⏲∫",
+ "adapt_delay": "Temps d'attente (secondes) entre allumage de la lumière et éclairage adaptatif en appliquant des changements. Ça pourrait aider à éviter de filmer. ⏲∫",
+ "interval": "Fréquence pour adapter les lumières, en quelques secondes. 🔄",
+ "transition": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑",
+ "initial_transition": "Durée de la première transition lorsque les feux tournent de `off` à `on` en quelques secondes. ⏲∫",
+ "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴",
+ "sleep_rgb_or_color_temp": "Utilisez soit `\"rgb_color\"` ou `\"color_temp\"` en mode sommeil. 🌙",
+ "sleep_color_temp": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴",
+ "brightness_mode_time_dark": "(Ignoré si `brightness_mode='default'`) La durée en quelques secondes pour remonter/verser la luminosité avant/après le lever du soleil/sunset. 📈📉"
+ }
+ }
+ },
+ "error": {
+ "option_error": "Option non valide",
+ "entity_missing": "Une ou plusieurs entités lumineuses sélectionnées sont absentes de Home Assistant"
}
- }
},
- "error": {
- "option_error": "Option non valide",
- "entity_missing": "Une lumière sélectionnée n’a pas été trouvée"
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "adapt_delay": {
+ "name": "adapt_delay",
+ "description": "Temps d'attente (secondes) entre allumage de la lumière et éclairage adaptatif en appliquant des changements. Ça pourrait aider à éviter de filmer. ⏲∫"
+ },
+ "min_color_temp": {
+ "name": "min_color_temp",
+ "description": "Température de couleur la plus chaude de Kelvin. 🔥"
+ },
+ "autoreset_control_seconds": {
+ "name": "autoreset_control_seconds",
+ "description": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫"
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "description": "ID d'entité du commutateur. 📝"
+ },
+ "include_config_in_attributes": {
+ "name": "include_config_in_attributes",
+ "description": "Afficher toutes les options en tant qu'attributs sur le commutateur dans l'assistant d'accueil lorsqu'il s'agit de \" vérité \" . 📝"
+ },
+ "max_color_temp": {
+ "name": "max_color_temp",
+ "description": "Température de couleur la plus froide en Kelvin. assemblage"
+ },
+ "only_once": {
+ "name": "only_once",
+ "description": "Adaptez les lumières seulement lorsqu'elles sont allumées ( \" vrai \" ) ou continuez à les adapter ( \" faux \" ). 🔄"
+ },
+ "prefer_rgb_color": {
+ "name": "prefer_rgb_color",
+ "description": "Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible."
+ },
+ "send_split_delay": {
+ "name": "send_split_delay",
+ "description": "Délai (ms) entre `separate_turn_on_commands` pour les lumières qui ne supportent pas la luminosité simultanée et le réglage de couleur. ⏲∫"
+ },
+ "separate_turn_on_commands": {
+ "name": "separate_turn_on_commands",
+ "description": "Utilisez des appels séparés `light.turn_on` pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀"
+ },
+ "sleep_brightness": {
+ "name": "sleep_brightness",
+ "description": "Pourcentage de luminosité des lumières en mode sommeil. 😴"
+ },
+ "sunrise_time": {
+ "name": "sunrise_time",
+ "description": "Réglez un temps fixe (HH:MM:SS) pour le lever du soleil. 🌅"
+ },
+ "sunset_time": {
+ "name": "coucher de soleil_time",
+ "description": "Réglez un temps fixe (HH:MM:SS) pour le coucher de soleil. 🌇"
+ },
+ "sunset_offset": {
+ "name": "coucher de soleil_offset",
+ "description": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰"
+ },
+ "take_over_control": {
+ "name": "take_over_control",
+ "description": "Adaptable Éclairage si une autre source appelle `light.turn_on` alors que les lumières sont allumées et adaptées. Notez que cela appelle `homeassistant.update_entity` chaque `intervale`"
+ },
+ "use_defaults": {
+ "name": "use_defaults",
+ "description": "Définit les valeurs par défaut non spécifiées dans cet appel de service. Options : \"current\" (par défaut, conserve les valeurs courantes), \"factory\" (réinitialisation des défauts documentés), ou \"configuration\" (revertissement des défauts de configuration). écrasement"
+ },
+ "sleep_rgb_or_color_temp": {
+ "name": "sleep_rgb_or_color_temp",
+ "description": "Utilisez soit `\"rgb_color\"` ou `\"color_temp\"` en mode sommeil. 🌙"
+ },
+ "turn_on_lights": {
+ "description": "Que ce soit pour allumer des lumières qui sont actuellement éteintes. 🔆",
+ "name": "turn_on_lights"
+ },
+ "initial_transition": {
+ "description": "Durée de la première transition lorsque les feux tournent de `off` à `on` en quelques secondes. ⏲∫",
+ "name": "initial_transition"
+ },
+ "sleep_transition": {
+ "description": "Durée de la transition lorsque \"mode de repos\" est activé en quelques secondes. 😴",
+ "name": "sleep_transition"
+ },
+ "max_brightness": {
+ "description": "Pourcentage de luminosité maximum. personnalisation",
+ "name": "max_brightness"
+ },
+ "min_brightness": {
+ "description": "Pourcentage de luminosité minimum. personnalisation",
+ "name": "min_brightness"
+ },
+ "sleep_rgb_color": {
+ "description": "Couleur RGB en mode sommeil (utilisé lorsque `sleep_rgb_or_color_temp` est \"rgb_color\").",
+ "name": "sleep_rgb_color"
+ },
+ "sleep_color_temp": {
+ "description": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴",
+ "name": "sleep_color_temp"
+ },
+ "sunrise_offset": {
+ "description": "Réglez le lever de soleil avec un décalage positif ou négatif en quelques secondes. ⏰",
+ "name": "sunrise_offset"
+ },
+ "max_sunrise_time": {
+ "description": "Définir le dernier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil antérieures. 🌅",
+ "name": "max_sunrise_time"
+ },
+ "min_sunset_time": {
+ "description": "Réglez le premier temps de coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇",
+ "name": "min_sunset_time"
+ },
+ "detect_non_ha_changes": {
+ "description": "Détecte et arrête les adaptations pour non-léger. changement d'état. Besoins `take_over_control` activé. Certaines lumières pourraient faussement indiquer un état « sur », ce qui pourrait donner lieu à des lumières s'allumer de façon inattendue. Désactivez cette fonctionnalité si vous rencontrez de tels problèmes.",
+ "name": "detect_non_ha_changes"
+ },
+ "transition": {
+ "description": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑",
+ "name": "transition"
+ }
+ },
+ "name": "change_switch_settings",
+ "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration."
+ },
+ "apply": {
+ "fields": {
+ "entity_id": {
+ "name": "entity_id",
+ "description": "Le `entity_id` du commutateur avec les réglages à appliquer. 📝"
+ },
+ "lights": {
+ "name": "lumières",
+ "description": "Une lumière (ou une liste de lumières) pour appliquer les réglages. personnalisation"
+ },
+ "prefer_rgb_color": {
+ "name": "prefer_rgb_color",
+ "description": "Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible."
+ },
+ "transition": {
+ "name": "transition",
+ "description": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑"
+ },
+ "turn_on_lights": {
+ "name": "turn_on_lights",
+ "description": "Que ce soit pour allumer des lumières qui sont actuellement éteintes. 🔆"
+ },
+ "adapt_brightness": {
+ "description": "Que ce soit pour adapter la luminosité de la lumière. 🌞",
+ "name": "adapt_brightness"
+ },
+ "adapt_color": {
+ "description": "Que ce soit pour adapter la couleur sur les feux support.",
+ "name": "adapt_color"
+ }
+ },
+ "name": "applique",
+ "description": "Applique les réglages d'éclairage adaptatif actuels aux lumières."
+ },
+ "set_manual_control": {
+ "fields": {
+ "lights": {
+ "name": "lumières",
+ "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. personnalisation"
+ },
+ "manual_control": {
+ "name": "manual_control",
+ "description": "Que ce soit pour ajouter (« faux ») ou supprimer (« faux ») la lumière de la liste « manual_control ». 🔒"
+ },
+ "entity_id": {
+ "description": "Le `entity_id` du commutateur dans lequel (un) marquer la lumière comme étant `manuellement contrôlé`. 📝",
+ "name": "entity_id"
+ }
+ },
+ "description": "Marquer si une lumière est «manuellement contrôlée».",
+ "name": "set_manual_control"
+ }
}
- }
}
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index cc269685..39bee1e2 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -47,6 +47,9 @@
"transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)",
"adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.",
"adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer je de lichten in eerste instantie aanzet. Indien ingesteld op `true`, past AL zich alleen aan als `light.turn_on` wordt aangeroepen zonder kleur of helderheid op te geven. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Indien `false`, past AL zich aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. Moet 'take_over_control' ingeschakeld zijn. 🕵️ "
+ },
+ "data_description": {
+ "sunrise_offset": "Een zonsopgang met een positief of negatief offset in seconden. _"
}
}
},
@@ -60,6 +63,9 @@
"fields": {
"only_once": {
"description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄"
+ },
+ "sunrise_offset": {
+ "description": "Een zonsopgang met een positief of negatief offset in seconden. _"
}
}
}
From 2211aa07b5400fc59f7b2122ee9bceff48a9f645 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Tue, 15 Aug 2023 23:16:17 -0700
Subject: [PATCH 132/549] Revert "Translations update from Hosted Weblate (de,
nl, fr) (#755)" (#757)
This reverts commit 627c7a120c0ea54a9de6d881dd8ec896cb9dc145.
---
.../adaptive_lighting/translations/de.json | 317 +++---------------
.../adaptive_lighting/translations/fr.json | 310 +++--------------
.../adaptive_lighting/translations/nl.json | 6 -
3 files changed, 99 insertions(+), 534 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json
index abb12101..79eb0df8 100644
--- a/custom_components/adaptive_lighting/translations/de.json
+++ b/custom_components/adaptive_lighting/translations/de.json
@@ -1,269 +1,58 @@
{
- "title": "Verwendbare Beleuchtung",
- "config": {
- "step": {
- "user": {
- "title": "Wähl aus einen Namen für die Verwendbare Beleuchtung Instanz",
- "description": "Jede Instanz kann mehrfache Lichter zügeln!",
- "data": {
- "name": "Name"
- }
- }
- },
- "abort": {
- "already_configured": "Diese Vorrichtung ist schon konfiguriert"
+ "title": "Adaptive Lighting",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Benenne das Adaptive Lighting",
+ "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten",
+ "data": {
+ "name": "Name"
}
+ }
},
- "options": {
- "step": {
- "init": {
- "title": "Verwendbare Beleuchtung Optionen",
- "description": "Konfigurier eine Verwendbare Beleuchtung Komponente. Option Namen stimmen ab mit die YAML Lagen. Ob du hast diesen Eintrag definiert herein YAML, keine Optionen wollen fungieren hier. Für interaktiv Graphen #der demonstrieren Parameter Effekte, besuchen [dieses Web #App](https://basnijholt.github.io/Verwendbar-Beleuchtung). Für ferner Details, seht die [offizielle Dokumentation](https://github.com/basnijholt/Verwendbar-Beleuchtung#readme).",
- "data": {
- "lights": "Lichter: Liste von licht Entität_ids zu sein reguliert (darf sein leer). 🌟",
- "initial_transition": "Einleitender_Wechsel",
- "sleep_transition": "Schlaf_Wechsel",
- "interval": "Intervall",
- "max_brightness": "max_Helligkeit: #Höchster Helligkeit Prozentsatz. 💡",
- "max_color_temp": "max_Farbe_Aushilfskraft: #Kalt Farbe Temperatur in #Kelvin. ❄️",
- "min_brightness": "min_Helligkeit: Minimaler Helligkeit Prozentsatz. 💡",
- "min_color_temp": "min_Farbe_Aushilfskraft: #Warm Farbe Temperatur in #Kelvin. 🔥",
- "only_once": "Nur_einmal: Adaptier nur Lichter als sind angemacht sie (`wahr`) oder Unterhalt adaptieren ihnen (`falsch`). 🔄",
- "prefer_rgb_color": "Bevorzug_rgb_Farbe: Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈",
- "separate_turn_on_commands": "Getrennte_Drehung_auf_Befehle: Nutzung trennen `Licht.Drehung_holt ab` weiter Farbe und Helligkeit, gebraucht für einige Licht Typen. 🔀",
- "send_split_delay": "#Senden_geteilt_Aufschub",
- "sleep_brightness": "Schlaf_Helligkeit",
- "sleep_rgb_or_color_temp": "Schlaf_rgb_oder_Farbe_Aushilfskraft",
- "sleep_rgb_color": "Schlaf_rgb_Farbe",
- "sleep_color_temp": "Schlaf_Farbe_Aushilfskraft",
- "sunrise_offset": "Sonnenaufgang_Verschiebung",
- "sunrise_time": "Sonnenaufgang_Zeit",
- "max_sunrise_time": "max_Sonnenaufgang_Zeit",
- "sunset_offset": "Abendrot_Verschiebung",
- "sunset_time": "Abendrot_Zeit",
- "min_sunset_time": "min_Abendrot_Zeit",
- "take_over_control": "Nimm_über_Aufsicht: Schalte aus Verwendbare Beleuchtung ob anderen Quelle Anrufe `hell.Drehung_auf` während sind Lichter weiter und sein adaptiert. Note dass diese Anrufe `homeassistant.Update_Entität` jedes `Intervall`! 🔒",
- "detect_non_ha_changes": "Finde heraus_nicht_ha_Änderungen: Findet heraus und hält an Bearbeitungen für nicht-`Licht.Drehung_auf` staatlich Änderungen. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ Vorsicht: ⚠️ Einige Lichter dürfen hindeuten #kein 'weiter' Staat, #welche konnte resultieren in Lichter anmachen unerwartet. Schalte aus diesen Charakterzug ob du begegnest solche Sachverhalte.",
- "transition": "Wechsel",
- "adapt_delay": "Adaptier_Aufschub",
- "skip_redundant_commands": "Sprung_redundant_Befehle: Sprung senden Bearbeitung gebietet wessen anrichten schon staatlich #angleichen das Lichts gekannt Staat. Minimiert Netzwerk Verkehr und verbessert die Bearbeitung responsivity in einigen Situationen. 📉Schalte aus ob physische Licht Staaten steigen aus von synchronisieren mit HAaufgezeichnet haben Staat.",
- "autoreset_control_seconds": "autoreset_Aufsicht_Sekunden",
- "brightness_mode": "Helligkeit_Verfahren",
- "brightness_mode_time_dark": "Helligkeit_Verfahren_Zeit_Finsternis",
- "brightness_mode_time_light": "Helligkeit_Verfahren_Zeit_Licht",
- "max_sunset_time": "max_Abendrot_Zeit",
- "transition_until_sleep": "Wechsel_bis_Schlaf: Wann aktiviert, Verwendbare Beleuchtung will spendieren schlafen Lagen da das Minimum, übergehen zu diesen Werten nach Abendrot. 🌙",
- "min_sunrise_time": "min_Sonnenaufgang_Zeit",
- "adapt_only_on_bare_turn_on": "Adaptier_nur_auf_kahl_Drehung_weiter: Wann drehen weiter Lichter anfänglich. Ob gesetzt zu `wahr`, #AL adaptiert nur ob `Licht.Drehung_ist aufgerufen` weiter ohne präzisieren Farbe oder Helligkeit. ❌🌈 Dies #z.B., verhindert als Bearbeitung aktivierend eine Szene. Ob `falsch`, #AL adaptiert #ohne Rücksicht auf von dem Vorliegen von Farbe oder Helligkeit in der einleitenden `Bedienung_Daten`. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ ",
- "intercept": "Unterbrich: Unterbrich und adaptier `Licht.Drehung_auf` Anrufe zu aktivieren momentane Farbe und Helligkeit Bearbeitung. 🏎️ Schalte aus für Lichter #der unterstützen nicht `Licht.Drehung_weiter` mit Farbe und Helligkeit.",
- "multi_light_intercept": "multi_Licht_unterbricht: Unterbrich und adaptier `Licht.Drehung_auf` Anrufe jenes Soll mehrfache Lichter. ➗⚠️ Dies darf resultieren in #aufspalten ein lediges `Licht.Drehung_auf` Anruf hinein mehrfach Anrufe, #z.B., als sind Lichter in verschieden Schalter. Bedürft `unterbrechen` zu sein aktiviert.",
- "include_config_in_attributes": "Schließ ein_config_in_Attribute: Vorstellung jede Optionen da Attribute auf dem Schalter in Heim Stellvertretend als gesetzt zu `wahr`. 📝"
- },
- "data_description": {
- "initial_transition": "Dauer von dem ersten Wechsel zündet an als Drehung von `ab` zu `weiter` in Sekunden. ⏲️",
- "sleep_brightness": "Helligkeit Prozentsatz von Lichter in schlafen Verfahren. 😴",
- "sleep_rgb_color": "#RGB Farbe in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist \"rgb_Farbe\"). 🌈",
- "sleep_transition": "Dauer von Wechsel schläft \"als Verfahren\" ist #festknebeln in Sekunden. 😴",
- "sunrise_time": "Gesetzt eine feste Zeit (HH:MM:SS) Für Sonnenaufgang. 🌅",
- "min_sunrise_time": "Setzte den frühesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für später Sonnenaufgänge. 🌅",
- "interval": "Häufigkeit zu adaptieren die Lichter, in Sekunden. 🔄",
- "transition": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑",
- "sleep_rgb_or_color_temp": "Nutzung entweder `\"rgb_Farbe\"` oder `\"Farbe_Aushilfskraft\"` in Schlaf Verfahren. 🌙",
- "sleep_color_temp": "Farbe Temperatur in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist `Farbe_Aushilfskraft`) in #Kelvin. 😴",
- "max_sunrise_time": "Setzte den spätesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für früher Sonnenaufgänge. 🌅",
- "sunrise_offset": "Pass an Sonnenaufgang Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰",
- "sunset_time": "Gesetzt eine feste Zeit (HH:MM:SS) Für Abendrot. 🌇",
- "min_sunset_time": "Setzte das früheste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für später Abendrot. 🌇",
- "max_sunset_time": "Setzte das späteste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für früher Abendrot. 🌇",
- "sunset_offset": "Pass an Abendrot Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰",
- "brightness_mode": "Helligkeit Verfahren zu benutzen. Mögliche Werte sind `#voreingestellt`, `geradlinig`, und `tanh` (Nutzungen `Helligkeit_Verfahren_Zeit_Finsternis` und `Helligkeit_Verfahren_Zeit_hell`). 📈",
- "brightness_mode_time_dark": "(Überhört ob `Helligkeit_Verfahren='#voreingestellt'`) Die Dauer in Sekunden zu #Nepp oben/#vor herunter die Helligkeit/nach Sonnenaufgang/Abendrot. 📈📉",
- "brightness_mode_time_light": "(Überhört ob `Helligkeit_Verfahren='#voreingestellt'`) Die Dauer in Sekunden zu #Nepp oben/#nachdem herunter die Helligkeit/vor Sonnenaufgang/Abendrot. 📈📉.",
- "autoreset_control_seconds": "Automatisch #rücksetzen die manuelle Aufsicht nach einer Nummer von Sekunden. Apparat zu 0 zu ausschalten. ⏲️",
- "send_split_delay": "Aufschub (ms) zwischen `getrennt_Drehung_auf_Befehle` für Lichter #der unterstützen nicht zeitgleiche Helligkeit und Farbe Lage. ⏲️",
- "adapt_delay": "Wartezeit Zeit (Sekunden) zwischen Licht macht an und Verwendbare Beleuchtung bewerbend Änderungen. Dürfen helfen zu vermeiden flackern. ⏲️"
- }
- }
- },
- "error": {
- "option_error": "Kränkliche Option",
- "entity_missing": "#Man oder #mehr ausgewählt Licht Entitäten fehlen von Heim Assistenten"
- }
- },
- "services": {
- "change_switch_settings": {
- "fields": {
- "use_defaults": {
- "description": "Setzt nicht die #voreingestellt Werte präzisiert in diesen Bedienung Anruf. Optionen: \"Lauf\" (#voreingestellt, hält fest gängige Werte), \"Fabrik\" (#Nachstellung zu dokumentieren Vorgaben), oder \"Konfiguration\" (fällt zurück zu schalten config Vorgaben). ⚙️",
- "name": "Benutzen_Vorgaben"
- },
- "include_config_in_attributes": {
- "description": "Vorstellung jede Optionen da Attribute auf dem Schalter in Heim Stellvertretend als gesetzt zu `wahr`. 📝",
- "name": "Schließ ein_config_in_Attribute"
- },
- "turn_on_lights": {
- "description": "Ob zu anmachen Lichter jener ist zurzeit ab. 🔆",
- "name": "Drehung_auf_Lichter"
- },
- "initial_transition": {
- "description": "Dauer von dem ersten Wechsel zündet an als Drehung von `ab` zu `weiter` in Sekunden. ⏲️",
- "name": "Einleitender_Wechsel"
- },
- "sleep_transition": {
- "description": "Dauer von Wechsel schläft \"als Verfahren\" ist #festknebeln in Sekunden. 😴",
- "name": "Schlaf_Wechsel"
- },
- "max_brightness": {
- "description": "#Höchster Helligkeit Prozentsatz. 💡",
- "name": "max_Helligkeit"
- },
- "max_color_temp": {
- "description": "#Kalt Farbe Temperatur in #Kelvin. ❄️",
- "name": "max_Farbe_Aushilfskraft"
- },
- "min_brightness": {
- "description": "Minimaler Helligkeit Prozentsatz. 💡",
- "name": "min_Helligkeit"
- },
- "only_once": {
- "description": "Adaptier nur Lichter als sind angemacht sie (`wahr`) oder Unterhalt adaptieren ihnen (`falsch`). 🔄",
- "name": "Nur_einmal"
- },
- "prefer_rgb_color": {
- "description": "Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈",
- "name": "Bevorzug_rgb_Farbe"
- },
- "send_split_delay": {
- "description": "Aufschub (ms) zwischen `getrennt_Drehung_auf_Befehle` für Lichter #der unterstützen nicht zeitgleiche Helligkeit und Farbe Lage. ⏲️",
- "name": "#Senden_geteilt_Aufschub"
- },
- "sleep_brightness": {
- "description": "Helligkeit Prozentsatz von Lichter in schlafen Verfahren. 😴",
- "name": "Schlaf_Helligkeit"
- },
- "sleep_rgb_or_color_temp": {
- "description": "Nutzung entweder `\"rgb_Farbe\"` oder `\"Farbe_Aushilfskraft\"` in Schlaf Verfahren. 🌙",
- "name": "Schlaf_rgb_oder_Farbe_Aushilfskraft"
- },
- "sleep_rgb_color": {
- "description": "#RGB Farbe in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist \"rgb_Farbe\"). 🌈",
- "name": "Schlaf_rgb_Farbe"
- },
- "sleep_color_temp": {
- "description": "Farbe Temperatur in Schlaf Verfahren (benutzt als `schlafen_rgb_oder_Farbe_Aushilfskraft` ist `Farbe_Aushilfskraft`) in #Kelvin. 😴",
- "name": "Schlaf_Farbe_Aushilfskraft"
- },
- "sunrise_offset": {
- "description": "Pass an Sonnenaufgang Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰",
- "name": "Sonnenaufgang_Verschiebung"
- },
- "sunrise_time": {
- "description": "Gesetzt eine feste Zeit (HH:MM:SS) Für Sonnenaufgang. 🌅",
- "name": "Sonnenaufgang_Zeit"
- },
- "sunset_offset": {
- "description": "Pass an Abendrot Zeit mit ein bejahendes oder verneinte Verschiebung in Sekunden. ⏰",
- "name": "Abendrot_Verschiebung"
- },
- "take_over_control": {
- "description": "Schalte aus Verwendbare Beleuchtung ob anderen Quelle Anrufe `hell.Drehung_auf` während sind Lichter weiter und sein adaptiert. Note dass diese Anrufe `homeassistant.Update_Entität` jedes `Intervall`! 🔒",
- "name": "Nimm_über_Aufsicht"
- },
- "detect_non_ha_changes": {
- "description": "Findet heraus und hält an Bearbeitungen für nicht-`Licht.Drehung_auf` staatlich Änderungen. Notwendigkeiten `nehmen_über_Aufsicht` aktiviert. 🕵️ Vorsicht: ⚠️ Einige Lichter dürfen hindeuten #kein 'weiter' Staat, #welche konnte resultieren in Lichter anmachen unerwartet. Schalte aus diesen Charakterzug ob du begegnest solche Sachverhalte.",
- "name": "Finde heraus_nicht_ha_Änderungen"
- },
- "transition": {
- "description": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑",
- "name": "Wechsel"
- },
- "adapt_delay": {
- "description": "Wartezeit Zeit (Sekunden) zwischen Licht macht an und Verwendbare Beleuchtung bewerbend Änderungen. Dürfen helfen zu vermeiden flackern. ⏲️",
- "name": "Adaptier_Aufschub"
- },
- "autoreset_control_seconds": {
- "description": "Automatisch #rücksetzen die manuelle Aufsicht nach einer Nummer von Sekunden. Apparat zu 0 zu ausschalten. ⏲️",
- "name": "autoreset_Aufsicht_Sekunden"
- },
- "min_color_temp": {
- "description": "#Warm Farbe Temperatur in #Kelvin. 🔥",
- "name": "min_Farbe_Aushilfskraft"
- },
- "separate_turn_on_commands": {
- "description": "Nutzung trennen `Licht.Drehung_holt ab` weiter Farbe und Helligkeit, gebraucht für einige Licht Typen. 🔀",
- "name": "Getrennte_Drehung_auf_Befehle"
- },
- "sunset_time": {
- "description": "Gesetzt eine feste Zeit (HH:MM:SS) Für Abendrot. 🌇",
- "name": "Abendrot_Zeit"
- },
- "max_sunrise_time": {
- "description": "Setzte den spätesten virtuellen Sonnenaufgang Zeit (HH:MM:SS), Erlauben für früher Sonnenaufgänge. 🌅",
- "name": "max_Sonnenaufgang_Zeit"
- },
- "min_sunset_time": {
- "description": "Setzte das früheste virtuelle Abendrot Zeit (HH:MM:SS), Erlauben für später Abendrot. 🌇",
- "name": "min_Abendrot_Zeit"
- },
- "entity_id": {
- "name": "Entität_id",
- "description": "Entität ID von dem Schalter. 📝"
- }
- },
- "name": "Änderung_Schalter_Lagen",
- "description": "Änderung irgendwelche Lagen wolltest du mögen in dem Schalter. Alle Optionen sind hier #dieselbe wie herein die config Strömung."
- },
- "set_manual_control": {
- "fields": {
- "manual_control": {
- "name": "Manuelle_Aufsicht",
- "description": "Ob zu zufügen (\"wahr\") oder entfernen (\"falsch\") das Licht von der \"manuellen_Aufsicht\" Liste. 🔒"
- },
- "lights": {
- "name": "Lichter",
- "description": "Entität_id(s) von Lichter, ob nicht präzisiert, alle Lichter in dem Schalter ist ausgewählt. 💡"
- },
- "entity_id": {
- "description": "Die `Entität_id` von dem Schalter in #welche zu (un)zensiert das lichtes da sein `manuell reguliert`. 📝",
- "name": "Entität_id"
- }
- },
- "name": "Gesetzt_manuelle_Aufsicht",
- "description": "Mark ob ist ein Licht 'manuell reguliert'."
- },
- "apply": {
- "fields": {
- "prefer_rgb_color": {
- "name": "Bevorzug_rgb_Farbe",
- "description": "Ob zu bevorzugen #RGB Farbe Anpassung über licht Farbe Temperatur als möglich. 🌈"
- },
- "transition": {
- "name": "Wechsel",
- "description": "Dauer von Wechsel zündet an als Änderung, in Sekunden. 🕑"
- },
- "adapt_brightness": {
- "name": "Adaptier_Helligkeit",
- "description": "Ob zu adaptieren die Helligkeit von dem Licht. 🌞"
- },
- "entity_id": {
- "name": "Entität_id",
- "description": "Die `Entität_id` von dem Schalter mit den Lagen zu bewerben. 📝"
- },
- "lights": {
- "name": "Lichter",
- "description": "Ein lichtes (oder Liste von Lichter) zu bewerben die Lagen zu. 💡"
- },
- "turn_on_lights": {
- "name": "Drehung_auf_Lichter",
- "description": "Ob zu anmachen Lichter jener ist zurzeit ab. 🔆"
- },
- "adapt_color": {
- "description": "Ob zu adaptieren die Farbe auf #stützend Lichter. 🌈",
- "name": "Adaptier_Farbe"
- }
- },
- "name": "Bewirb",
- "description": "Bewirbt den Lauf Verwendbar Beleuchtung Lagen zu Lichter."
- }
+ "abort": {
+ "already_configured": "Gerät ist bereits konfiguriert!"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Adaptive Lighting Optionen",
+ "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.",
+ "data": {
+ "lights": "Lichter",
+ "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt",
+ "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)",
+ "interval": "interval, Zeit zwischen Updates des Switches",
+ "max_brightness": "max_brightness, maximale Helligkeit in %",
+ "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin",
+ "min_brightness": "min_brightness, minimale Helligkeit in %",
+ "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin",
+ "only_once": "only_once, passe die Lichter nur beim Einschalten an",
+ "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich",
+ "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.",
+ "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.",
+ "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'",
+ "sleep_rgb_color": "sleep_rgb_color, in RGB",
+ "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin",
+ "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden",
+ "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)",
+ "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)",
+ "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden",
+ "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)",
+ "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)",
+ "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.",
+ "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)",
+ "transition": "transition, Wechselzeit in Sekunden",
+ "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.",
+ "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben."
+ }
+ }
+ },
+ "error": {
+ "option_error": "Fehlerhafte Option",
+ "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden"
+ }
+ }
}
diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json
index 6e4c7a8f..a41d84a0 100644
--- a/custom_components/adaptive_lighting/translations/fr.json
+++ b/custom_components/adaptive_lighting/translations/fr.json
@@ -1,269 +1,51 @@
{
- "title": "Éclairage adaptatif",
- "config": {
- "step": {
- "user": {
- "title": "Choisissez un nom pour l'instance Adaptive Lighting",
- "description": "Chaque instance peut contenir plusieurs lumières !",
- "data": {
- "name": "Nom"
- }
- }
- },
- "abort": {
- "already_configured": "Cet appareil est déjà configuré"
+ "title": "Éclairage adaptatif",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Choisissez un nom pour cette instance d'éclairage adaptatif",
+ "description": "Choisissez un nom pour cette instance. Vous pouvez configurer plusieurs instances d'éclairage adaptatif, chacune pouvant contrôler plusieurs lampes !",
+ "data": {
+ "name": "Nom"
}
+ }
},
- "options": {
- "step": {
- "init": {
- "title": "Options d ' éclairage adaptatifs",
- "description": "Configurer un adaptatif Élément d'éclairage. Les noms d'options correspondent aux réglages YAML. Si vous avez défini cette entrée dans YAML, aucune option n'apparaîtra ici. Pour les graphiques interactifs qui montrent les effets des paramètres, visitez [cette application Web](https://basnijholt.github.io/adaptive-lighting). Pour plus de détails, voir la [document officiel](https://github.com/basnijholt/adaptive-lighting#readme).",
- "data": {
- "lights": "lumières: List of light entity_ids to be controlled (may be empty). 🌟",
- "initial_transition": "initial_transition",
- "sleep_transition": "sleep_transition",
- "interval": "intervalle",
- "max_brightness": "max_brightness: pourcentage de luminosité maximum. personnalisation",
- "max_color_temp": "max_color_temp: Température de couleur la plus froide en Kelvin. assemblage",
- "min_brightness": "min_brightness: Pourcentage de luminosité minimum. personnalisation",
- "min_color_temp": "min_color_temp: Température de couleur la plus chaude de Kelvin. 🔥",
- "only_once": "seulement_une fois: Adaptez les lumières seulement lorsqu'elles sont allumées ( \" vrai \" ) ou continuez à les adapter ( \" faux \" ). 🔄",
- "prefer_rgb_color": "prefer_rgb_color: Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible.",
- "separate_turn_on_commands": "separate_turn_on_commands: Utilisez des appels séparés `light.turn_on` pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀",
- "sleep_brightness": "sleep_brightness",
- "sleep_color_temp": "sleep_color_temp",
- "sunrise_offset": "sunrise_offset",
- "sunrise_time": "sunrise_time",
- "sunset_offset": "coucher de soleil_offset",
- "sunset_time": "coucher de soleil_time",
- "take_over_control": "take_over_control: Éclairage adaptatif désactive si une autre source appelle `light.turn_on` alors que les lumières sont allumées et adaptées. Notez que cela appelle `homeassistant.update_entity` chaque `intervale`",
- "detect_non_ha_changes": "detect_non_ha_changes: Détecte et arrête les adaptations pour non-lumière. changement d'état. Besoins `take_over_control` activé. Certaines lumières pourraient faussement indiquer un état « sur », ce qui pourrait donner lieu à des lumières s'allumer de façon inattendue. Désactivez cette fonctionnalité si vous rencontrez de tels problèmes.",
- "transition": "transition",
- "send_split_delay": "send_split_delay",
- "brightness_mode": "luminosité_mode",
- "brightness_mode_time_dark": "brightness_mode_time_dark",
- "brightness_mode_time_light": "luminosité_mode_time_light",
- "max_sunset_time": "max_sunset_time",
- "min_sunset_time": "min_sunset_time",
- "sleep_rgb_color": "sleep_rgb_color",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp",
- "transition_until_sleep": "transition_until_sleep: Lorsque cela est activé, l'éclairage adaptatif traitera les paramètres de sommeil comme le minimum, en passant à ces valeurs après le coucher du soleil. 🌙",
- "min_sunrise_time": "min_sunrise_time",
- "max_sunrise_time": "max_sunrise_time",
- "autoreset_control_seconds": "autoreset_control_seconds",
- "adapt_delay": "adapt_delay",
- "skip_redundant_commands": "skip_redundant_commands: Skip sending adaptation commands whose target state already equals the light's known state. Minimise le trafic réseau et améliore la réceptivité de l'adaptation dans certaines situations. 📉Disponible si les états de lumière physique sortent de synchronisation avec l'état enregistré de HA.",
- "intercept": "intercept: Intercepter et adapter les appels `light.turn_on` pour permettre une adaptation instantanée de la couleur et de la luminosité. ACIA Désactiver les lumières qui ne supportent pas `light.turn_on` avec couleur et luminosité.",
- "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quand on allume les lumières au départ. Si le paramètre est < < vrai > > , AL s ' adapte uniquement si l ' on invoque < < light.turn_on > > sans préciser la couleur ou la luminosité. ❌ Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si `false`, AL s'adapte indépendamment de la présence de couleur ou de luminosité dans le `service_data' initial. Besoins `take_over_control` activé. 🕵∫ ",
- "multi_light_intercept": "multi_light_intercept: Interceptez et adaptez `light.turn_on` les appels qui ciblent plusieurs lumières. ➗gène Cela pourrait permettre de diviser un seul appel `light.turn_on` en plusieurs appels, par exemple lorsque les lumières sont dans différents interrupteurs. Nécessite un < < intercept > > pour être activé.",
- "include_config_in_attributes": "include_config_in_attributes: Afficher toutes les options en tant qu'attributs sur le commutateur dans l'assistant d'accueil lorsqu'il s'agit de «true»"
- },
- "data_description": {
- "sleep_rgb_color": "Couleur RGB en mode sommeil (utilisé lorsque `sleep_rgb_or_color_temp` est \"rgb_color\").",
- "sleep_transition": "Durée de la transition lorsque \"mode de repos\" est activé en quelques secondes. 😴",
- "sunrise_time": "Réglez un temps fixe (HH:MM:SS) pour le lever du soleil. 🌅",
- "min_sunrise_time": "Définir le premier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil ultérieures. 🌅",
- "max_sunrise_time": "Définir le dernier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil antérieures. 🌅",
- "sunrise_offset": "Réglez le lever de soleil avec un décalage positif ou négatif en quelques secondes. ⏰",
- "sunset_time": "Réglez un temps fixe (HH:MM:SS) pour le coucher de soleil. 🌇",
- "min_sunset_time": "Réglez le premier temps de coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇",
- "max_sunset_time": "Définir le dernier coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil antérieurs. 🌇",
- "sunset_offset": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰",
- "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont < < par défaut > > , < < linéaire > > et < < parois > > , < < par défaut > > , et < < par coup > > , < < par défaut > > et par > par > . 📈",
- "brightness_mode_time_light": "(Ignoré si `brightness_mode='default'`) La durée en quelques secondes pour augmenter/verser la luminosité après/avant le lever du soleil/sunset. 📈📉.",
- "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫",
- "send_split_delay": "Délai (ms) entre `separate_turn_on_commands` pour les lumières qui ne supportent pas la luminosité simultanée et le réglage de couleur. ⏲∫",
- "adapt_delay": "Temps d'attente (secondes) entre allumage de la lumière et éclairage adaptatif en appliquant des changements. Ça pourrait aider à éviter de filmer. ⏲∫",
- "interval": "Fréquence pour adapter les lumières, en quelques secondes. 🔄",
- "transition": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑",
- "initial_transition": "Durée de la première transition lorsque les feux tournent de `off` à `on` en quelques secondes. ⏲∫",
- "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴",
- "sleep_rgb_or_color_temp": "Utilisez soit `\"rgb_color\"` ou `\"color_temp\"` en mode sommeil. 🌙",
- "sleep_color_temp": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴",
- "brightness_mode_time_dark": "(Ignoré si `brightness_mode='default'`) La durée en quelques secondes pour remonter/verser la luminosité avant/après le lever du soleil/sunset. 📈📉"
- }
- }
- },
- "error": {
- "option_error": "Option non valide",
- "entity_missing": "Une ou plusieurs entités lumineuses sélectionnées sont absentes de Home Assistant"
- }
- },
- "services": {
- "change_switch_settings": {
- "fields": {
- "adapt_delay": {
- "name": "adapt_delay",
- "description": "Temps d'attente (secondes) entre allumage de la lumière et éclairage adaptatif en appliquant des changements. Ça pourrait aider à éviter de filmer. ⏲∫"
- },
- "min_color_temp": {
- "name": "min_color_temp",
- "description": "Température de couleur la plus chaude de Kelvin. 🔥"
- },
- "autoreset_control_seconds": {
- "name": "autoreset_control_seconds",
- "description": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫"
- },
- "entity_id": {
- "name": "entity_id",
- "description": "ID d'entité du commutateur. 📝"
- },
- "include_config_in_attributes": {
- "name": "include_config_in_attributes",
- "description": "Afficher toutes les options en tant qu'attributs sur le commutateur dans l'assistant d'accueil lorsqu'il s'agit de \" vérité \" . 📝"
- },
- "max_color_temp": {
- "name": "max_color_temp",
- "description": "Température de couleur la plus froide en Kelvin. assemblage"
- },
- "only_once": {
- "name": "only_once",
- "description": "Adaptez les lumières seulement lorsqu'elles sont allumées ( \" vrai \" ) ou continuez à les adapter ( \" faux \" ). 🔄"
- },
- "prefer_rgb_color": {
- "name": "prefer_rgb_color",
- "description": "Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible."
- },
- "send_split_delay": {
- "name": "send_split_delay",
- "description": "Délai (ms) entre `separate_turn_on_commands` pour les lumières qui ne supportent pas la luminosité simultanée et le réglage de couleur. ⏲∫"
- },
- "separate_turn_on_commands": {
- "name": "separate_turn_on_commands",
- "description": "Utilisez des appels séparés `light.turn_on` pour la couleur et la luminosité, nécessaires pour certains types de lumière. 🔀"
- },
- "sleep_brightness": {
- "name": "sleep_brightness",
- "description": "Pourcentage de luminosité des lumières en mode sommeil. 😴"
- },
- "sunrise_time": {
- "name": "sunrise_time",
- "description": "Réglez un temps fixe (HH:MM:SS) pour le lever du soleil. 🌅"
- },
- "sunset_time": {
- "name": "coucher de soleil_time",
- "description": "Réglez un temps fixe (HH:MM:SS) pour le coucher de soleil. 🌇"
- },
- "sunset_offset": {
- "name": "coucher de soleil_offset",
- "description": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰"
- },
- "take_over_control": {
- "name": "take_over_control",
- "description": "Adaptable Éclairage si une autre source appelle `light.turn_on` alors que les lumières sont allumées et adaptées. Notez que cela appelle `homeassistant.update_entity` chaque `intervale`"
- },
- "use_defaults": {
- "name": "use_defaults",
- "description": "Définit les valeurs par défaut non spécifiées dans cet appel de service. Options : \"current\" (par défaut, conserve les valeurs courantes), \"factory\" (réinitialisation des défauts documentés), ou \"configuration\" (revertissement des défauts de configuration). écrasement"
- },
- "sleep_rgb_or_color_temp": {
- "name": "sleep_rgb_or_color_temp",
- "description": "Utilisez soit `\"rgb_color\"` ou `\"color_temp\"` en mode sommeil. 🌙"
- },
- "turn_on_lights": {
- "description": "Que ce soit pour allumer des lumières qui sont actuellement éteintes. 🔆",
- "name": "turn_on_lights"
- },
- "initial_transition": {
- "description": "Durée de la première transition lorsque les feux tournent de `off` à `on` en quelques secondes. ⏲∫",
- "name": "initial_transition"
- },
- "sleep_transition": {
- "description": "Durée de la transition lorsque \"mode de repos\" est activé en quelques secondes. 😴",
- "name": "sleep_transition"
- },
- "max_brightness": {
- "description": "Pourcentage de luminosité maximum. personnalisation",
- "name": "max_brightness"
- },
- "min_brightness": {
- "description": "Pourcentage de luminosité minimum. personnalisation",
- "name": "min_brightness"
- },
- "sleep_rgb_color": {
- "description": "Couleur RGB en mode sommeil (utilisé lorsque `sleep_rgb_or_color_temp` est \"rgb_color\").",
- "name": "sleep_rgb_color"
- },
- "sleep_color_temp": {
- "description": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴",
- "name": "sleep_color_temp"
- },
- "sunrise_offset": {
- "description": "Réglez le lever de soleil avec un décalage positif ou négatif en quelques secondes. ⏰",
- "name": "sunrise_offset"
- },
- "max_sunrise_time": {
- "description": "Définir le dernier temps de lever de soleil virtuel (HHH:MM:SS), permettant des levures de soleil antérieures. 🌅",
- "name": "max_sunrise_time"
- },
- "min_sunset_time": {
- "description": "Réglez le premier temps de coucher de soleil virtuel (HHH:MM:SS), permettant des couchers de soleil ultérieurs. 🌇",
- "name": "min_sunset_time"
- },
- "detect_non_ha_changes": {
- "description": "Détecte et arrête les adaptations pour non-léger. changement d'état. Besoins `take_over_control` activé. Certaines lumières pourraient faussement indiquer un état « sur », ce qui pourrait donner lieu à des lumières s'allumer de façon inattendue. Désactivez cette fonctionnalité si vous rencontrez de tels problèmes.",
- "name": "detect_non_ha_changes"
- },
- "transition": {
- "description": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑",
- "name": "transition"
- }
- },
- "name": "change_switch_settings",
- "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration."
- },
- "apply": {
- "fields": {
- "entity_id": {
- "name": "entity_id",
- "description": "Le `entity_id` du commutateur avec les réglages à appliquer. 📝"
- },
- "lights": {
- "name": "lumières",
- "description": "Une lumière (ou une liste de lumières) pour appliquer les réglages. personnalisation"
- },
- "prefer_rgb_color": {
- "name": "prefer_rgb_color",
- "description": "Que ce soit pour préférer le réglage de couleur RGB sur la température de couleur claire si possible."
- },
- "transition": {
- "name": "transition",
- "description": "Durée de la transition lorsque les lumières changent, en quelques secondes. 🕑"
- },
- "turn_on_lights": {
- "name": "turn_on_lights",
- "description": "Que ce soit pour allumer des lumières qui sont actuellement éteintes. 🔆"
- },
- "adapt_brightness": {
- "description": "Que ce soit pour adapter la luminosité de la lumière. 🌞",
- "name": "adapt_brightness"
- },
- "adapt_color": {
- "description": "Que ce soit pour adapter la couleur sur les feux support.",
- "name": "adapt_color"
- }
- },
- "name": "applique",
- "description": "Applique les réglages d'éclairage adaptatif actuels aux lumières."
- },
- "set_manual_control": {
- "fields": {
- "lights": {
- "name": "lumières",
- "description": "entity_id(s) of lights, if not specified, all lights in the switch are selected. personnalisation"
- },
- "manual_control": {
- "name": "manual_control",
- "description": "Que ce soit pour ajouter (« faux ») ou supprimer (« faux ») la lumière de la liste « manual_control ». 🔒"
- },
- "entity_id": {
- "description": "Le `entity_id` du commutateur dans lequel (un) marquer la lumière comme étant `manuellement contrôlé`. 📝",
- "name": "entity_id"
- }
- },
- "description": "Marquer si une lumière est «manuellement contrôlée».",
- "name": "set_manual_control"
- }
+ "abort": {
+ "already_configured": "Cet appareil est déjà configuré"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Options d'éclairage adaptatif",
+ "description": "Tous les paramètres de l'instance d'éclairage adaptatif. Les noms des options correspondent aux paramètres YAML. Aucune option n'est affichée si l'entrée adaptive_lighting est définie dans votre configuration YAML.",
+ "data": {
+ "lights": "lights : Les lampes à contrôler",
+ "initial_transition": "initial_transition : Transition (en secondes) lorsque l'état d'une lampe passe d'« éteinte » à « allumée ».",
+ "sleep_transition": "sleep_transition : Transition (en secondes) lorsque « sleep_state » est commuté.",
+ "interval": "interval : Temps (en secondes) entre deux mises à jour du commutateur.",
+ "max_brightness": "max_brightness : Luminosité maximale des lampes (en pourcentage) au cours d'un cycle.",
+ "max_color_temp": "max_color_temp : Couleur la plus froide (en kelvins) du cycle de température de couleur.",
+ "min_brightness": "min_brightness : Luminosité minimale des lampes (en pourcentage) au cours d'un cycle.",
+ "min_color_temp": "min_color_temp : Couleur la plus chaude (en kelvins) du cycle de température de couleur.",
+ "only_once": "only_once : Adapter les lampes uniquement au moment où elles sont allumées.",
+ "prefer_rgb_color": "prefer_rgb_color : Utiliser « rgb_color » plutôt que « color_temp » lorsque cela est possible.",
+ "separate_turn_on_commands": "separate_turn_on_commands : Séparer les commandes pour chaque attribut (couleur, luminosité, etc.) de « light.turn_on » (nécessaire pour certaines lampes).",
+ "sleep_brightness": "sleep_brightness : Luminosité (en pourcentage) du mode nuit.",
+ "sleep_color_temp": "sleep_color_temp : Température de couleur (en kelvins) du mode nuit.",
+ "sunrise_offset": "sunrise_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au lever du soleil.",
+ "sunrise_time": "sunrise_time : Heure (HH:MM:SS) du lever du soleil. Si « None », utilise l'heure correspondant à votre emplacement.",
+ "sunset_offset": "sunset_offset : Décalage (en secondes [- : passé, + : futur]) du cycle par rapport au coucher du soleil.",
+ "sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.",
+ "take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.",
+ "detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)",
+ "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes."
+ }
+ }
+ },
+ "error": {
+ "option_error": "Option non valide",
+ "entity_missing": "Une lumière sélectionnée n’a pas été trouvée"
+ }
+ }
}
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index 39bee1e2..cc269685 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -47,9 +47,6 @@
"transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)",
"adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.",
"adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer je de lichten in eerste instantie aanzet. Indien ingesteld op `true`, past AL zich alleen aan als `light.turn_on` wordt aangeroepen zonder kleur of helderheid op te geven. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Indien `false`, past AL zich aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. Moet 'take_over_control' ingeschakeld zijn. 🕵️ "
- },
- "data_description": {
- "sunrise_offset": "Een zonsopgang met een positief of negatief offset in seconden. _"
}
}
},
@@ -63,9 +60,6 @@
"fields": {
"only_once": {
"description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄"
- },
- "sunrise_offset": {
- "description": "Een zonsopgang met een positief of negatief offset in seconden. _"
}
}
}
From 3757a668291a2058ace0731ea1f5e465ec1a345a Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Thu, 17 Aug 2023 06:04:59 +0200
Subject: [PATCH 133/549] Translated using Weblate (German) (#758)
Currently translated at 55.5% (85 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/de/
Co-authored-by: Michel Balzer
---
.../adaptive_lighting/translations/de.json | 111 ++++++++++--------
1 file changed, 60 insertions(+), 51 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json
index 79eb0df8..154d1fe0 100644
--- a/custom_components/adaptive_lighting/translations/de.json
+++ b/custom_components/adaptive_lighting/translations/de.json
@@ -1,58 +1,67 @@
{
- "title": "Adaptive Lighting",
- "config": {
- "step": {
- "user": {
- "title": "Benenne das Adaptive Lighting",
- "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten",
- "data": {
- "name": "Name"
+ "title": "Adaptive Lighting",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Benenne das Adaptive Lighting",
+ "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten",
+ "data": {
+ "name": "Name"
+ }
+ }
+ },
+ "abort": {
+ "already_configured": "Gerät ist bereits konfiguriert!"
}
- }
},
- "abort": {
- "already_configured": "Gerät ist bereits konfiguriert!"
- }
- },
- "options": {
- "step": {
- "init": {
- "title": "Adaptive Lighting Optionen",
- "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.",
- "data": {
- "lights": "Lichter",
- "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt",
- "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)",
- "interval": "interval, Zeit zwischen Updates des Switches",
- "max_brightness": "max_brightness, maximale Helligkeit in %",
- "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin",
- "min_brightness": "min_brightness, minimale Helligkeit in %",
- "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin",
- "only_once": "only_once, passe die Lichter nur beim Einschalten an",
- "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich",
- "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.",
- "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.",
- "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'",
- "sleep_rgb_color": "sleep_rgb_color, in RGB",
- "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin",
- "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden",
- "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)",
- "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)",
- "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden",
- "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)",
- "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)",
- "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.",
- "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)",
- "transition": "transition, Wechselzeit in Sekunden",
- "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.",
- "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben."
+ "options": {
+ "step": {
+ "init": {
+ "title": "Adaptive Lighting Optionen",
+ "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.",
+ "data": {
+ "lights": "Lichter",
+ "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt",
+ "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)",
+ "interval": "interval, Zeit zwischen Updates des Switches",
+ "max_brightness": "max_brightness, maximale Helligkeit in %",
+ "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin",
+ "min_brightness": "min_brightness, minimale Helligkeit in %",
+ "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin",
+ "only_once": "only_once, passe die Lichter nur beim Einschalten an",
+ "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich",
+ "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.",
+ "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.",
+ "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'",
+ "sleep_rgb_color": "sleep_rgb_color, in RGB",
+ "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin",
+ "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden",
+ "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)",
+ "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)",
+ "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden",
+ "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)",
+ "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)",
+ "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.",
+ "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)",
+ "transition": "transition, Wechselzeit in Sekunden",
+ "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.",
+ "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben."
+ }
+ }
+ },
+ "error": {
+ "option_error": "Fehlerhafte Option",
+ "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden"
}
- }
},
- "error": {
- "option_error": "Fehlerhafte Option",
- "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden"
+ "services": {
+ "apply": {
+ "fields": {
+ "lights": {
+ "description": "Ein Licht (oder eine Lichtliste), um die Einstellungen anzuwenden. RECHT"
+ }
+ }
+ }
}
- }
}
From 239aae8ed7501063b0fbec247534f3dc7af7b3a3 Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Thu, 17 Aug 2023 23:43:44 +0200
Subject: [PATCH 134/549] Translated using Weblate (Dutch) (#760)
* Translated using Weblate (Dutch)
Currently translated at 57.5% (88 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
* Translated using Weblate (Dutch)
Currently translated at 58.1% (89 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
---------
Co-authored-by: Bas Nijholt
---
.../adaptive_lighting/translations/nl.json | 134 ++++++++++--------
1 file changed, 72 insertions(+), 62 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index cc269685..6faff432 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -1,67 +1,77 @@
{
- "title": "Adaptieve verlichting",
- "config": {
- "step": {
- "user": {
- "title": "Kies een naam voor de adaptieve verlichting integratie",
- "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!",
- "data": {
- "name": "Naam"
- }
- }
- },
- "abort": {
- "already_configured": "Dit apparaat is al geconfigureerd"
+ "title": "Adaptieve verlichting",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Kies een naam voor de adaptieve verlichting integratie",
+ "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!",
+ "data": {
+ "name": "Naam"
}
+ }
},
- "options": {
- "step": {
- "init": {
- "title": "Adaptieve verlichting instellingen",
- "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.",
- "data": {
- "lights": "Lichten",
- "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)",
- "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)",
- "interval": "interval: Tijd tussen switch-updates. (seconden)",
- "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)",
- "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)",
- "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)",
- "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)",
- "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.",
- "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.",
- "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).",
- "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.",
- "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'",
- "sleep_rgb_color": "sleep_rgb_color, in RGB",
- "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)",
- "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)",
- "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
- "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
- "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)",
- "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
- "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)",
- "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.",
- "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)",
- "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)",
- "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.",
- "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Wanneer je de lichten in eerste instantie aanzet. Indien ingesteld op `true`, past AL zich alleen aan als `light.turn_on` wordt aangeroepen zonder kleur of helderheid op te geven. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Indien `false`, past AL zich aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. Moet 'take_over_control' ingeschakeld zijn. 🕵️ "
- }
- }
- },
- "error": {
- "option_error": "Ongeldige optie",
- "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant"
- }
- },
- "services": {
- "change_switch_settings": {
- "fields": {
- "only_once": {
- "description": "Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). 🔄"
- }
- }
- }
+ "abort": {
+ "already_configured": "Dit apparaat is al geconfigureerd"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Adaptieve verlichting instellingen",
+ "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.",
+ "data": {
+ "lights": "Lichten",
+ "initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)",
+ "sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)",
+ "interval": "interval: Tijd tussen switch-updates. (seconden)",
+ "max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)",
+ "max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)",
+ "min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)",
+ "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)",
+ "only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.",
+ "prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.",
+ "separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).",
+ "send_split_delay": "send_split_delay: wacht tussen commando's (milliseconden), wanneer separate_turn_on_commands wordt gebruikt. Kan ervoor zorgen dat beide commando's correct door de lamp worden afgehandeld.",
+ "sleep_brightness": "sleep_brightness, helderheidsinstelling voor slaapstand. (%)",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, gebruik 'rgb_color' of 'color_temp'",
+ "sleep_rgb_color": "sleep_rgb_color, in RGB",
+ "sleep_color_temp": "sleep_color_temp: Kleurtemperatuurinstelling voor slaapstand. (kelvin)",
+ "sunrise_offset": "sunrise_offset: Hoe lang voor(-) of na(+) zonsopgang uitvoeren (+/- seconden)",
+ "sunrise_time": "sunrise_time: Handmatige wijziging van de zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
+ "max_sunrise_time": "max_sunrise_time: handmatige aanpassing van de maximale zonsopgangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
+ "sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)",
+ "sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
+ "min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)",
+ "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.",
+ "detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)",
+ "transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)",
+ "adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past AL alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past AL aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ "
+ },
+ "data_description": {
+ "sunrise_offset": "Pas de zonsopkomsttijd aan met een positieve of negatieve offset in seconden. ⏰",
+ "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰"
+ }
+ }
+ },
+ "error": {
+ "option_error": "Ongeldige optie",
+ "entity_missing": "Een of meer geselecteerde lichtentiteiten ontbreken in Home Assistant"
+ }
+ },
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "only_once": {
+ "description": "Pas lampen alleen aan wanneer ze zijn ingeschakeld (`true`) of blijf ze aanpassen (`false`). 🔄"
+ },
+ "sunrise_offset": {
+ "description": "Pas de tijd van zonsopkomst aan met een positieve of negatieve verschuiving in seconden. ⏰"
+ },
+ "sunset_offset": {
+ "description": "Pas de tijd van zonsondergang aan met een positieve of negatieve offset in seconden. ⏰"
+ }
+ }
+ }
+ }
}
From 857ac10856106549f5ca200674074f728cd5f79c Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Fri, 18 Aug 2023 04:45:24 +0200
Subject: [PATCH 135/549] Translations update from Hosted Weblate (#761)
* Translated using Weblate (Dutch)
Currently translated at 57.5% (88 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
* Translated using Weblate (Dutch)
Currently translated at 58.1% (89 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
* Translated using Weblate (Dutch)
Currently translated at 62.0% (95 of 153 strings)
Translation: Adaptive Lighting/Adaptive Lighting
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
---------
Co-authored-by: Bas Nijholt
---
.../adaptive_lighting/translations/nl.json | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index 6faff432..2e2e64aa 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -46,11 +46,17 @@
"detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)",
"transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)",
"adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.",
- "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past AL alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past AL aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ "
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past AL alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past AL aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ ",
+ "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve Verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙",
+ "skip_redundant_commands": "skip_redundant_commands: Sla het verzenden van aanpassingscommando's over waarvan de doelstatus al gelijk is aan de bekende status van de lamp. Minimaliseert netwerkverkeer en verbetert de responsiviteit van de aanpassing in sommige situaties. 📉Schakel uit als de fysieke lichtstatus niet meer synchroon loopt met de door HA geregistreerde status.",
+ "intercept": "intercept: Onderschep en pas `light.turn_on` oproepen aan om directe kleur- en helderheidsaanpassing mogelijk te maken. 🏎️ Schakel uit voor lampen die `light.turn_on` niet ondersteunen met kleur en helderheid.",
+ "include_config_in_attributes": "include_config_in_attributes: Toon alle opties als attributen op de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝",
+ "multi_light_intercept": "multi_light_intercept: Onderschep en pas `light.turn_on` oproepen aan die gericht zijn op meerdere lampen. ➗⚠️ Dit kan resulteren in het opsplitsen van een enkele `light.turn_on` call in meerdere calls, bijvoorbeeld wanneer lampen zich in verschillende schakelaars bevinden. Vereist dat `intercept` is ingeschakeld."
},
"data_description": {
"sunrise_offset": "Pas de zonsopkomsttijd aan met een positieve of negatieve offset in seconden. ⏰",
- "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰"
+ "sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰",
+ "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄"
}
}
},
From 65b82b605e8c86cafa05ec481077481beffe329b Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Fri, 18 Aug 2023 19:25:55 +0200
Subject: [PATCH 136/549] Translated using Weblate (Polish)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently translated at 64.0% (98 of 153 strings)
Co-authored-by: Łukasz Marek
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/pl.json | 47 +++++++++++++------
1 file changed, 32 insertions(+), 15 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json
index 99a97e3c..038d9bee 100644
--- a/custom_components/adaptive_lighting/translations/pl.json
+++ b/custom_components/adaptive_lighting/translations/pl.json
@@ -3,8 +3,8 @@
"config": {
"step": {
"user": {
- "title": "Wybierz nazwę grupy dla Adaptacyjnego oświetlenia",
- "description": "Wybierz nazwę dla grupy. Możesz użyć wiele grup Adaptacyjnego oświetlenia, każda może mieć dowolną konfigurację świateł!",
+ "title": "Wybierz nazwę dla tej instancji Adaptacyjnego oświetlenia",
+ "description": "Każda instancja może zawierać wiele świateł.",
"data": {
"name": "Nazwa"
}
@@ -18,34 +18,51 @@
"step": {
"init": {
"title": "Adaptacyjne oświetlenie opcje",
- "description": "Wszystkie ustawienia dla Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli masz wpis adaptive_lighting zdefiniowany w konfiguracji YAML.",
+ "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowany w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [this web app](https://basnijholt.github.io/adaptive-lighting). Aby zobaczyć więcej szczegółów odwiedź [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).",
"data": {
- "lights": "światła",
+ "lights": "lights: Lista entity_ids, które mają być kontrolowane (może być pusta). 🌟",
"initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)",
"sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)",
"interval": "interval: Time between switch updates. (sekund)",
- "max_brightness": "max_brightness: Highest brightness of lights during a cycle. (%)",
- "max_color_temp": "max_color_temp: Coldest hue of the color temperature cycle. (Kelvin)",
- "min_brightness": "min_brightness: Lowest brightness of lights during a cycle. (%)",
- "min_color_temp": "min_color_temp, Warmest hue of the color temperature cycle. (Kelvin)",
- "only_once": "only_once: Only adapt the lights when turning them on.",
- "prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' rather than 'color_temp' when possible.",
- "separate_turn_on_commands": "separate_turn_on_commands: Separate the commands for each attribute (color, brightness, etc.) in 'light.turn_on' (required for some lights).",
+ "max_brightness": "max_brightness: Maksymajna josność (w procentach). 💡",
+ "max_color_temp": "max_color_temp: Najzimniejsza temperatura barwowa (w Kelwinach). ❄️",
+ "min_brightness": "min_brightness: Minimalna jasność (w procentach). 💡",
+ "min_color_temp": "min_color_temp: Najcieplejsza temperatura barwowa (w Kelwinach). 🔥",
+ "only_once": "only_once: Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄",
+ "prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła.. 🌈",
+ "separate_turn_on_commands": "separate_turn_on_commands: Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀",
"sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)",
"sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)",
"sunrise_offset": "sunrise_offset: How long before(-) or after(+) to define the sunrise point of the cycle (+/- sekund)",
"sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
"sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)",
"sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
- "take_over_control": "take_over_control: If anything but Adaptive Lighting calls 'light.turn_on' when a light is already on, stop adapting that light until it (or the switch) toggles off -> on.",
- "detect_non_ha_changes": "detect_non_ha_changes: detects all >10% changes made to the lights (also outside of HA), requires 'take_over_control' to be enabled (calls 'homeassistant.update_entity' every 'interval'!)",
- "transition": "Transition time when applying a change to the lights (sekund)"
+ "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ że to wywołuje `homeassistant.update_entity` co`interval`! 🔒",
+ "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów.",
+ "transition": "Transition time when applying a change to the lights (sekund)",
+ "transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone `true` to Adaptacyjne oświetlenie włączy adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji gdy aktywowana jest scena. Gdy wyłączone `false`, Adaptacyjne oświetlenie włączy adaptacje niezależnie czy `service_data zawiera kolor lub jasność`. Potrzebuje włączonej opcji `take_over_control`. 🕵️ ",
+ "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przysadkach poprawia szybkość działania. 📉Wyłącz jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.",
+ "include_config_in_attributes": "include_config_in_attributes: Gdy włącone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝",
+ "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`aby błyskawicznie dostosować kolor i jasność . 🏎️ Wyłącz dla świateł które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.",
+ "multi_light_intercept": "multi_light_intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, które dotyczą wielu świateł. ➗⚠️ Może to powodować rozdzielenie pojedynczego wywołania `light.turn_on` na wiele wywołań, na przykład gdy światła są przypisane do rożnych instancji. Wymaga włączonej opcji `intercept`."
+ },
+ "data_description": {
+ "interval": "Częstotliwość adaptacji świateł w sekundach. 🔄",
+ "transition": "Długość tranzycji do nowego stanu w sekundach. 🕑",
+ "initial_transition": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`w sekundach. ⏲️",
+ "sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"`trybie spania. 🌙",
+ "sleep_color_temp": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴",
+ "sleep_rgb_color": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈",
+ "sleep_transition": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴",
+ "sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅",
+ "sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴"
}
}
},
"error": {
"option_error": "Błędne opcje",
- "entity_missing": "Nie znaleziono wybranego światła"
+ "entity_missing": "Jednego lub więcej wybranych świateł nie można znaleźć w Home Assistant"
}
}
}
From c53816224ca1cbbb001fed7f46a096251464730e Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Fri, 18 Aug 2023 19:25:55 +0200
Subject: [PATCH 137/549] Translated using Weblate (French)
Currently translated at 54.9% (84 of 153 strings)
Co-authored-by: Maxime Bailleul
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/fr.json | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json
index a41d84a0..c7835639 100644
--- a/custom_components/adaptive_lighting/translations/fr.json
+++ b/custom_components/adaptive_lighting/translations/fr.json
@@ -40,6 +40,10 @@
"take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.",
"detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)",
"transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes."
+ },
+ "data_description": {
+ "interval": "Fréquence d'adaptation des lumières, en secondes. 🔄",
+ "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴"
}
}
},
@@ -47,5 +51,14 @@
"option_error": "Option non valide",
"entity_missing": "Une lumière sélectionnée n’a pas été trouvée"
}
+ },
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "sleep_brightness": {
+ "description": "Pourcentage de luminosité des lumières en mode sommeil. 😴"
+ }
+ }
+ }
}
}
From 3042ac85ef28f4840df8ae832ee532ffc24081c1 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Fri, 18 Aug 2023 11:31:53 -0700
Subject: [PATCH 138/549] docs: add lukerix as a contributor for translation
(#763)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 5 ++++-
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index c264a714..cf49d196 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -458,6 +458,15 @@
"contributions": [
"design"
]
+ },
+ {
+ "login": "lukerix",
+ "name": "lukerix",
+ "avatar_url": "https://avatars.githubusercontent.com/u/93864731?v=4",
+ "profile": "https://github.com/lukerix",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 1997c351..5de65115 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -525,6 +525,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Mario Guggenberger 💻 |
 Kendell R 🎨 |
+
+  lukerix 🌍 |
+
From d6d7c4aee9b6d7bc069956e2c573672b45d6ca2b Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Fri, 18 Aug 2023 11:34:22 -0700
Subject: [PATCH 139/549] docs: add michelbalzer as a contributor for
translation (#764)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index cf49d196..64eff534 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -467,6 +467,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "michelbalzer",
+ "name": "Michel Balzer",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1337412?v=4",
+ "profile": "https://michelbalzer.de",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 5de65115..e221cdd3 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -527,6 +527,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 lukerix 🌍 |
+  Michel Balzer 🌍 |
From a10f7e2550f03567de56a62cef0c57a4f5c5933e Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Fri, 18 Aug 2023 23:43:27 -0700
Subject: [PATCH 140/549] docs: add Mexx62 as a contributor for translation
(#765)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt
---
.all-contributorsrc | 9 +++++++++
README.md | 1 +
2 files changed, 10 insertions(+)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 64eff534..ae9114e2 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -468,6 +468,15 @@
"translation"
]
},
+ {
+ "login": "Mexx62",
+ "name": "Maxime Bailleul",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8066485?v=4",
+ "profile": "https://github.com/Mexx62",
+ "contributions": [
+ "translation"
+ ]
+ },
{
"login": "michelbalzer",
"name": "Michel Balzer",
diff --git a/README.md b/README.md
index e221cdd3..f8386453 100644
--- a/README.md
+++ b/README.md
@@ -527,6 +527,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 lukerix 🌍 |
+  Maxime Bailleul 🌍 |
 Michel Balzer 🌍 |
From be440b96411c5879f6ddf2f71d532e486d04ef82 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Sat, 19 Aug 2023 08:43:34 +0200
Subject: [PATCH 141/549] Translated using Weblate (Polish)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently translated at 76.4% (117 of 153 strings)
Translated using Weblate (Polish)
Currently translated at 69.2% (106 of 153 strings)
Translated using Weblate (Polish)
Currently translated at 65.3% (100 of 153 strings)
Co-authored-by: Hosted Weblate
Co-authored-by: Łukasz Marek
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/pl.json | 37 ++++++++++++++++++-
1 file changed, 36 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json
index 038d9bee..be3b5763 100644
--- a/custom_components/adaptive_lighting/translations/pl.json
+++ b/custom_components/adaptive_lighting/translations/pl.json
@@ -56,7 +56,20 @@
"sleep_rgb_color": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈",
"sleep_transition": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴",
"sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅",
- "sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴"
+ "sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴",
+ "min_sunrise_time": "Ustaw czas najwcześniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na opóźnienie wschodu słońca. 🌅",
+ "max_sunrise_time": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅",
+ "sunrise_offset": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰",
+ "sunset_time": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇",
+ "min_sunset_time": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇",
+ "brightness_mode": "Tryb ustawianie jasności. Dostępne opcje `default`, `linear`, i `tanh` (używa `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈",
+ "max_sunset_time": "Ustaw czas najpóźniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na przyspieszenie zachodu słońca. 🌇",
+ "sunset_offset": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰",
+ "brightness_mode_time_dark": "(pomijany gdy`brightness_mode='default'`) Czas w sekundach kiedy jasność będzie: zwiększana przed wschodem słońca/zmniejszana po zachodzie słońca. 📈📉",
+ "brightness_mode_time_light": "(pomijany gdy`brightness_mode='default'`) Czas w sekundach kiedy jasność będzie: zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉",
+ "autoreset_control_seconds": "Czas po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️",
+ "send_split_delay": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️",
+ "adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Morze pomóc zredukować migotanie. ⏲️"
}
}
},
@@ -64,5 +77,27 @@
"option_error": "Błędne opcje",
"entity_missing": "Jednego lub więcej wybranych świateł nie można znaleźć w Home Assistant"
}
+ },
+ "services": {
+ "apply": {
+ "description": "Stosuje bieżące ustawienia Adaptacyjnego oświetlenia do świateł.",
+ "fields": {
+ "entity_id": {
+ "description": "`entity_id` przełącznika, którego ustawienia mają być zastosowane. 📝"
+ },
+ "lights": {
+ "description": "Światło(albo lista świateł), do których mają być zastosowane ustawieniam 💡"
+ },
+ "transition": {
+ "description": "Czas tranzycji przy zmianie ustawień (w sekundach). 🕑"
+ },
+ "adapt_color": {
+ "description": "Czy adaptować kolor światła"
+ },
+ "adapt_brightness": {
+ "description": "Czy adaptować jasność swiatła. 🌞"
+ }
+ }
+ }
}
}
From e86744c6a3ad93b1e411c9b506b5b9e7cbca2db6 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Sat, 19 Aug 2023 08:43:34 +0200
Subject: [PATCH 142/549] Translated using Weblate (Dutch)
Currently translated at 68.6% (105 of 153 strings)
Co-authored-by: Bas Rutjes
Co-authored-by: Hosted Weblate
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/nl.json | 36 +++++++++++++++++--
1 file changed, 34 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index 2e2e64aa..2a64b971 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -4,7 +4,7 @@
"step": {
"user": {
"title": "Kies een naam voor de adaptieve verlichting integratie",
- "description": "Kies een naam voor deze integratie. U kunt verschillende integratie van Adaptieve verlichting uitvoeren, elk van deze kan meerdere lichten bevatten!",
+ "description": "Elk exemplaar kan meerdere lichten bevatten!",
"data": {
"name": "Naam"
}
@@ -56,7 +56,9 @@
"data_description": {
"sunrise_offset": "Pas de zonsopkomsttijd aan met een positieve of negatieve offset in seconden. ⏰",
"sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰",
- "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄"
+ "interval": "Frequentie om de lampen aan te passen, in seconden. 🔄",
+ "sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴",
+ "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen."
}
}
},
@@ -76,6 +78,36 @@
},
"sunset_offset": {
"description": "Pas de tijd van zonsondergang aan met een positieve of negatieve offset in seconden. ⏰"
+ },
+ "sleep_transition": {
+ "description": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴"
+ },
+ "entity_id": {
+ "description": "entiteit_id van de schakelaar. 📝"
+ },
+ "transition": {
+ "description": "Duur van de overgang in seconden, als lampen aanpassen. 🕑"
+ },
+ "autoreset_control_seconds": {
+ "description": "Herstel de handmatige bediening na een aantal seconden. Stel in op 0 om uit te schakelen."
+ }
+ }
+ },
+ "apply": {
+ "fields": {
+ "lights": {
+ "description": "Een lamp (of een lijst van lampen) waarop de instellingen worden toegepast."
+ },
+ "transition": {
+ "description": "Duur van de overgang in seconden, als lampen aanpassen. 🕑"
+ }
+ },
+ "description": "Past de huidige Adaptive Lights instellingen toe op de lampen."
+ },
+ "set_manual_control": {
+ "fields": {
+ "lights": {
+ "description": "entiteit_id(s) van de lamp(en), indien niets wordt gespecificeerd, worden alle lampen in de schakelaar geselecteerd. 💡"
}
}
}
From 2ab247243dca70fd8966d7b13fa4650a06c81613 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Sat, 19 Aug 2023 08:43:34 +0200
Subject: [PATCH 143/549] Translated using Weblate (French)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently translated at 67.9% (104 of 153 strings)
Translated using Weblate (French)
Currently translated at 67.9% (104 of 153 strings)
Co-authored-by: Hosted Weblate
Co-authored-by: Loïc R
Co-authored-by: Maxime Bailleul
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/fr.json | 46 +++++++++++++++++--
1 file changed, 43 insertions(+), 3 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json
index c7835639..e9bb3710 100644
--- a/custom_components/adaptive_lighting/translations/fr.json
+++ b/custom_components/adaptive_lighting/translations/fr.json
@@ -39,16 +39,23 @@
"sunset_time": "sunset_time : Heure (HH:MM:SS) du coucher du soleil. Si « None », utilise l'heure correspondant à votre emplacement.",
"take_over_control": "take_over_control : Si quelque chose d'autre que l'éclairage adaptatif appelle « light.turn_on » alors qu'une lampe est déjà allumée, cesser d'adapter cette lampe jusqu'à ce qu'elle (ou le commutateur) soit éteinte puis rallumée.",
"detect_non_ha_changes": "detect_non_ha_changes : Détecter tout changement de plus de 10 % appliqué aux lampes (même en dehors de HA). Nécessite que « take_over_control » soit activé. (Appelle « homeassistant.update_entity » tous les « interval » !)",
- "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes."
+ "transition": "transition : Durée de la transition (en secondes) des changements appliqués aux lampes.",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quand on allume les lumières au départ. Si le paramètre est < < vrai > > , AL s ' adapte uniquement si l ' on invoque < < light.turn_on > > sans préciser la couleur ou la luminosité. ❌ Ceci, par exemple, empêche l'adaptation lors de l'activation d'une scène. Si `false`, AL s'adapte indépendamment de la présence de couleur ou de luminosité dans le `service_data' initial. Besoins `take_over_control` activé. 🕵∫ "
},
"data_description": {
"interval": "Fréquence d'adaptation des lumières, en secondes. 🔄",
- "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴"
+ "sleep_brightness": "Pourcentage de luminosité des lumières en mode sommeil. 😴",
+ "autoreset_control_seconds": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫",
+ "sunset_offset": "Réglez le temps de coucher avec un décalage positif ou négatif en quelques secondes. ⏰",
+ "brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont < < par défaut > > , < < linéaire > > et < < parois > > , < < par défaut > > , et < < par coup > > , < < par défaut > > et par > par > . 📈",
+ "send_split_delay": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲",
+ "sleep_color_temp": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴",
+ "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰"
}
}
},
"error": {
- "option_error": "Option non valide",
+ "option_error": "Option invalide",
"entity_missing": "Une lumière sélectionnée n’a pas été trouvée"
}
},
@@ -57,6 +64,39 @@
"fields": {
"sleep_brightness": {
"description": "Pourcentage de luminosité des lumières en mode sommeil. 😴"
+ },
+ "only_once": {
+ "description": "Adapter les lumières seulement quand elles sont allumées (\"vrai\") ou quel que soit leur état (\"faux\")."
+ },
+ "sunrise_offset": {
+ "description": "Ajustez l'heure de lever de soleil avec un décalage positif ou négatif en secondes. ⏰"
+ },
+ "max_color_temp": {
+ "description": "Température de couleur la plus froide en Kelvin. assemblage"
+ },
+ "send_split_delay": {
+ "description": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲"
+ },
+ "detect_non_ha_changes": {
+ "description": "Détecte et arrête les adaptations pour un changement d'état autre que \"light.turn_on\". Nécessite \"take_over_control\" activé. 🕵️ Attention: ⚠️Certaines lumières pourraient faussement indiquer un état \"on\", ce qui pourrait donner lieu à des allumages inattendus. Désactivez cette fonctionnalité si vous rencontrez ce problème."
+ },
+ "autoreset_control_seconds": {
+ "description": "Réinitialiser automatiquement la commande manuelle après un certain nombre de secondes. Définir à 0 pour désactiver. ⏲∫"
+ },
+ "sunset_offset": {
+ "description": "Ajustez l'heure de coucher de soleil avec un décalage positif ou négatif en secondes. ⏰"
+ },
+ "sleep_color_temp": {
+ "description": "Température de couleur en mode sommeil (utilisé lorsque \"sleep_rgb_or_color_temp\" est défini sur \"color_temp\") en Kelvin. 😴"
+ }
+ },
+ "description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration."
+ },
+ "apply": {
+ "description": "Applique les réglages d'éclairage adaptatif actuels aux lumières.",
+ "fields": {
+ "lights": {
+ "description": "Une lumière (ou une liste de lumières) pour appliquer les réglages. personnalisation"
}
}
}
From a4f435f662da7d635968df69ba409679c3229d71 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Sun, 20 Aug 2023 17:52:05 +0200
Subject: [PATCH 144/549] Translated using Weblate (Polish)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently translated at 85.6% (131 of 153 strings)
Co-authored-by: Łukasz Marek
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/pl.json | 50 ++++++++++++++++++-
1 file changed, 48 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json
index be3b5763..6bbd2523 100644
--- a/custom_components/adaptive_lighting/translations/pl.json
+++ b/custom_components/adaptive_lighting/translations/pl.json
@@ -24,7 +24,7 @@
"initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)",
"sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)",
"interval": "interval: Time between switch updates. (sekund)",
- "max_brightness": "max_brightness: Maksymajna josność (w procentach). 💡",
+ "max_brightness": "max_brightness: Maksymalna jasność (w procentach). 💡",
"max_color_temp": "max_color_temp: Najzimniejsza temperatura barwowa (w Kelwinach). ❄️",
"min_brightness": "min_brightness: Minimalna jasność (w procentach). 💡",
"min_color_temp": "min_color_temp: Najcieplejsza temperatura barwowa (w Kelwinach). 🔥",
@@ -43,7 +43,7 @@
"transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙",
"adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone `true` to Adaptacyjne oświetlenie włączy adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji gdy aktywowana jest scena. Gdy wyłączone `false`, Adaptacyjne oświetlenie włączy adaptacje niezależnie czy `service_data zawiera kolor lub jasność`. Potrzebuje włączonej opcji `take_over_control`. 🕵️ ",
"skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przysadkach poprawia szybkość działania. 📉Wyłącz jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.",
- "include_config_in_attributes": "include_config_in_attributes: Gdy włącone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝",
+ "include_config_in_attributes": "include_config_in_attributes: Gdy włączone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝",
"intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`aby błyskawicznie dostosować kolor i jasność . 🏎️ Wyłącz dla świateł które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.",
"multi_light_intercept": "multi_light_intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, które dotyczą wielu świateł. ➗⚠️ Może to powodować rozdzielenie pojedynczego wywołania `light.turn_on` na wiele wywołań, na przykład gdy światła są przypisane do rożnych instancji. Wymaga włączonej opcji `intercept`."
},
@@ -96,6 +96,52 @@
},
"adapt_brightness": {
"description": "Czy adaptować jasność swiatła. 🌞"
+ },
+ "prefer_rgb_color": {
+ "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła. 🌈"
+ },
+ "turn_on_lights": {
+ "description": "Czy włączyć światła, które są aktualnie wyłączone? 🔆"
+ }
+ }
+ },
+ "set_manual_control": {
+ "description": "Zaznacza czy światło jest 'recznie sterowane'.",
+ "fields": {
+ "entity_id": {
+ "description": "`entity_id` encji przełącznika, w której należy odznaczyć flagę `ręczne sterowanie`. 📝"
+ },
+ "lights": {
+ "description": "`entity_id` świateł, dla których należy odznaczyć flagę `ręczne sterowanie`.💡Gdy lista będzie pusta wszystkie światła będą brane pod uwagę."
+ },
+ "manual_control": {
+ "description": "Dodaj (\"true\") albo usuń (\"false\") światło z listy \"ręczne sterowanie\". 🔒"
+ }
+ }
+ },
+ "change_switch_settings": {
+ "description": "Zmienia dowolny parametr w przełączniku. Wszystkie opcje są takie same jak w konfiguracji.",
+ "fields": {
+ "entity_id": {
+ "description": "ID encji przełacznika. 📝"
+ },
+ "use_defaults": {
+ "description": "Jak mają się zmienić ustawienia, które nie są wyszczególnione w tym wywołaniu. Opcje: \"current\" (domyślne, pozostawia obecne ustawienia), \"factory\" (przywraca ustawienia z dokumentacji), albo \"configuration\" (przywraca wartości z konfiguracji przelacznika). ⚙️"
+ },
+ "include_config_in_attributes": {
+ "description": "Gdy włączone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant."
+ },
+ "sleep_transition": {
+ "description": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴"
+ },
+ "max_brightness": {
+ "description": "Maksymalna jasność (w procentach). 💡"
+ },
+ "turn_on_lights": {
+ "description": "Czy włączyć światła, które są aktualnie wyłączone? 🔆"
+ },
+ "initial_transition": {
+ "description": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`(w sekundach). ⏲️"
}
}
}
From badf53427c1786a44a5fc52208e76fe72f9f5d3b Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Tue, 29 Aug 2023 04:20:26 +0200
Subject: [PATCH 145/549] Translated using Weblate (Catalan)
Currently translated at 60.1% (92 of 153 strings)
Added translation using Weblate (Catalan)
Co-authored-by: Gerard Rubio
Co-authored-by: Hosted Weblate
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ca/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/ca.json | 77 +++++++++++++++++++
1 file changed, 77 insertions(+)
create mode 100644 custom_components/adaptive_lighting/translations/ca.json
diff --git a/custom_components/adaptive_lighting/translations/ca.json b/custom_components/adaptive_lighting/translations/ca.json
new file mode 100644
index 00000000..11e6d43d
--- /dev/null
+++ b/custom_components/adaptive_lighting/translations/ca.json
@@ -0,0 +1,77 @@
+{
+ "title": "Il·luminació Adaptativa",
+ "options": {
+ "step": {
+ "init": {
+ "data_description": {
+ "initial_transition": "Durada de la primera transició quan els llums s'encenen de `off` a `on` en segons. ⏲️",
+ "sunset_offset": "Ajusta l'hora de la posta del sol amb una compensació positiva o negativa en segons. ⏰",
+ "send_split_delay": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️",
+ "sunrise_offset": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰",
+ "autoreset_control_seconds": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️",
+ "brightness_mode": "Mode de brillantor a utilitzar. Els valors possibles són `default`, \"linear\" i \"tanh\" (utilitza `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈",
+ "sleep_color_temp": "Temperatura de color en mode nit (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴",
+ "sleep_brightness": "Percentatge de brillantor de les llums en mode nit. 😴"
+ },
+ "title": "Opcions Il·luminació Adaptativa",
+ "data": {
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quan s'encenen les llums inicialment. Si el valor és `true`, AL adapta només si s'ha cridat `light.turn_on` sense especificar color o brillantor. ❌🌈 Això impedeix l'adaptació quan s'activa una escena. Si el valor és `false`, AL adapta independentment de la presencia de color o brillantor en les dades inicials `service_data`. Necessita `take_over_control` habilitat. 🕵️ ",
+ "detect_non_ha_changes": "detect_non_ha_changes: Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita `take_over_control` habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguessin inesperadament. Desactiva aquesta funció si trobes aquests problemes."
+ },
+ "description": "Configura un component d'Adaptive Lighting. Els noms de les opcions s'alineen amb la configuració de YAML. Si has definit aquesta entrada a YAML, aquí no apareixerà cap opció. Per veure gràfics interactius que demostren efectes de paràmetres, visita [aquesta aplicació web] (https://basnijholt.github.io/adaptive-lighting). Per a més detalls, pots veure la [documentació oficial] (https://github.com/basnijholt/adaptive-lighting#readme)."
+ }
+ }
+ },
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "only_once": {
+ "description": "Ajustar les llums només quan s'encenguin (`true`) o ajustar contínuament(`false`). 🔄"
+ },
+ "sleep_color_temp": {
+ "description": "Temperatura de color en mode nit (s'utilitza quan `sleep_rgb_or_color_temp` és `color_temp`) en Kelvin. 😴"
+ },
+ "sunrise_offset": {
+ "description": "Ajusta l'hora de sortida del sol amb una compensació positiva o negativa en segons. ⏰"
+ },
+ "sunset_offset": {
+ "description": "Ajusta l'hora de la posta del sol amb una compensació positiva o negativa en segons. ⏰"
+ },
+ "autoreset_control_seconds": {
+ "description": "Restableix automàticament el control manual al cap d'uns segons. Posar a 0 per desactivar. ⏲️"
+ },
+ "sleep_brightness": {
+ "description": "Percentatge de brillantor de les llums en mode nit. 😴"
+ },
+ "max_color_temp": {
+ "description": "Temperatura de color més freda en Kelvin. ❄️"
+ },
+ "send_split_delay": {
+ "description": "Retard (ms) entre `separate_turn_on_commands` per als llums que no admeten la configuració simultània de brillantor i color. ⏲️"
+ },
+ "detect_non_ha_changes": {
+ "description": "Detecta i atura les adaptacions per als canvis d'estat diferents a `light.turn_on`. Necessita `take_over_control` habilitat. 🕵️ Precaució: ⚠️ Alguns llums poden indicar falsament un estat \"encès\", cosa que podria provocar que els llums s'encenguessin inesperadament. Desactiva aquesta funció si trobes aquests problemes."
+ },
+ "take_over_control": {
+ "description": "Desactiva Adaptive Lighting si una altra font crida `light.turn_on` mentre els llums estan encesos i adaptats. Tingues en compte que això crida `homeassistant.update_entity` cada `interval`! 🔒"
+ }
+ },
+ "description": "Canvia les opcions de configuració que vulguis al commutador. Totes les opcions d'aquí són les mateixes que en el flux de configuració."
+ },
+ "apply": {
+ "fields": {
+ "lights": {
+ "description": "Una llum (o una llista de llums) a la qual aplicar la configuració. 💡"
+ }
+ },
+ "description": "Aplica la configuració actual d'Adaptive Lighting a les llums."
+ }
+ },
+ "config": {
+ "step": {
+ "user": {
+ "title": "Tria un nom per a la instància d'Adaptive Lighting"
+ }
+ }
+ }
+}
From 8b0616f184c2254c8e468e87b16cdf4ae298d614 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Tue, 29 Aug 2023 04:20:26 +0200
Subject: [PATCH 146/549] Translated using Weblate (Italian)
Currently translated at 56.2% (86 of 153 strings)
Co-authored-by: Enrico Gambini
Co-authored-by: Hosted Weblate
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/it/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/it.json | 101 ++++++++++--------
1 file changed, 57 insertions(+), 44 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json
index c1ae6163..c1dccbbe 100644
--- a/custom_components/adaptive_lighting/translations/it.json
+++ b/custom_components/adaptive_lighting/translations/it.json
@@ -1,52 +1,65 @@
{
- "title": "Illuminazione Adattiva",
- "config": {
- "step": {
- "user": {
- "title": "Scegli un nome per l'istanza di Illuminazione Adattiva",
- "description": "Scegli un nome per questa istanza. Puoi eseguire più istanze di Illuminazione adattiva, ognuna delle quali può contenere più luci!",
- "data": {
- "name": "Nome"
- }
+ "title": "Illuminazione Adattiva",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Scegli un nome per l'istanza di Illuminazione Adattiva",
+ "description": "Scegli un nome per questa istanza. Puoi eseguire più istanze di Illuminazione adattiva, ognuna delle quali può contenere più luci!",
+ "data": {
+ "name": "Nome"
}
- },
- "abort": {
- "already_configured": "Questo dispositivo è già stato configurato"
}
},
- "options": {
- "step": {
- "init": {
- "title": "Opzioni Illuminazione Adattiva",
- "description": "Tutte le opzioni per il componente Illuminazione Adattiva. I nomi delle opzioni corrispondono con le impostazioni YAML. Non sono mostrate opzioni se hai la voce adaptive-lighting definita nella tua configurazione YAML.",
- "data": {
- "lights": "luci",
- "initial_transition": "initial_transition: Quando le luci vengono accese (off -> on). (secondi)",
- "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)",
- "interval": "interval: Tempo tra i cambiamenti dello switch. (secondi)",
- "max_brightness": "max_brightness: Luminosità massima delle luci durante un ciclo. (%)",
- "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)",
- "min_brightness": "min_brightness: Luminosità minima delle luci durante un ciclo. (%)",
- "min_color_temp": "min_color_temp: Gradazione più calda del ciclo di temperatura del colore. (Kelvin)",
- "only_once": "only_once: Adatta le luci solo quando vengono accese.",
- "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.",
- "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).",
- "sleep_brightness": "sleep_brightness: Impostazione della luminosità per la modalità notturna. (%)",
- "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)",
- "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)",
- "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)",
- "sunset_offset": "sunset_offset: Imposta quanto anticipare(-) o ritardare(+) il tramonto nel ciclo (+/- secondi)",
- "sunset_time": "sunset_time: Imposta manualmente l'ora del tramonto, se 'None', usa l'ora effettiva del tramonto alla tua posizione (HH:MM:SS)",
- "take_over_control": "take_over_control: Se viene chiamato il servizio 'lights.turn_on' (non da Illuminazione Adattiva) quando una luce è già accesa, interrompi l'adattamento della luce finquando essa o l'interruttore non vengono riaccesi (off -> on.)",
- "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)",
- "transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)",
- "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii."
- }
+ "abort": {
+ "already_configured": "Questo dispositivo è già stato configurato"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Opzioni Illuminazione Adattiva",
+ "description": "Tutte le opzioni per il componente Illuminazione Adattiva. I nomi delle opzioni corrispondono con le impostazioni YAML. Non sono mostrate opzioni se hai la voce adaptive-lighting definita nella tua configurazione YAML.",
+ "data": {
+ "lights": "luci",
+ "initial_transition": "initial_transition: Quando le luci vengono accese (off -> on). (secondi)",
+ "sleep_transition": "sleep_transition: Quando 'sleep_state' cambia. (secondi)",
+ "interval": "interval: Tempo tra i cambiamenti dello switch. (secondi)",
+ "max_brightness": "max_brightness: Luminosità massima delle luci durante un ciclo. (%)",
+ "max_color_temp": "max_color_temp: Gradazione più fredda del ciclo di temperatura del colore. (Kelvin)",
+ "min_brightness": "min_brightness: Luminosità minima delle luci durante un ciclo. (%)",
+ "min_color_temp": "min_color_temp: Gradazione più calda del ciclo di temperatura del colore. (Kelvin)",
+ "only_once": "only_once: Adatta le luci solo quando vengono accese.",
+ "prefer_rgb_color": "prefer_rgb_color: Usa 'rgb_color' al posto di 'color_temp' quando possibile.",
+ "separate_turn_on_commands": "separate_turn_on_commands: Separa i comandi per ogni attributo (color, brightness, etc.) in 'light.turn_on' (richiesto per alcune luci).",
+ "sleep_brightness": "sleep_brightness: Impostazione della luminosità per la modalità notturna. (%)",
+ "sleep_color_temp": "sleep_color_temp: Impostazione della temperatura colore per la modalità notturna. (Kelvin)",
+ "sunrise_offset": "sunrise_offset: Imposta quanto anticipare(-) o ritardare(+) l'alba nel ciclo (+/- secondi)",
+ "sunrise_time": "sunrise_time: Imposta manualmente l'ora dell'alba, se 'None', usa l'ora effettiva dell'alba alla tua posizione (HH:MM:SS)",
+ "sunset_offset": "sunset_offset: Imposta quanto anticipare(-) o ritardare(+) il tramonto nel ciclo (+/- secondi)",
+ "sunset_time": "sunset_time: Imposta manualmente l'ora del tramonto, se 'None', usa l'ora effettiva del tramonto alla tua posizione (HH:MM:SS)",
+ "take_over_control": "take_over_control: Se viene chiamato il servizio 'lights.turn_on' (non da Illuminazione Adattiva) quando una luce è già accesa, interrompi l'adattamento della luce finquando essa o l'interruttore non vengono riaccesi (off -> on.)",
+ "detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)",
+ "transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)",
+ "adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii.",
+ "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙"
+ },
+ "data_description": {
+ "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰"
+ }
+ }
+ },
+ "error": {
+ "option_error": "Opzione non valida",
+ "entity_missing": "Non è stata trovata una luce selezionata"
+ }
+ },
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "only_once": {
+ "description": "Adatta le luci solo nel momento in cui vengono accese ('true') o continua ad adattarle ('false'). 🔄"
}
- },
- "error": {
- "option_error": "Opzione non valida",
- "entity_missing": "Non è stata trovata una luce selezionata"
}
}
}
+}
From 08a61767ad66961eebfd971bf3393d3830380873 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Tue, 29 Aug 2023 04:20:26 +0200
Subject: [PATCH 147/549] Translated using Weblate (Polish)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently translated at 100.0% (153 of 153 strings)
Co-authored-by: Hosted Weblate
Co-authored-by: Łukasz Marek
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/pl.json | 70 ++++++++++++++++++-
1 file changed, 68 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json
index 6bbd2523..6ad9be29 100644
--- a/custom_components/adaptive_lighting/translations/pl.json
+++ b/custom_components/adaptive_lighting/translations/pl.json
@@ -37,7 +37,7 @@
"sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
"sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)",
"sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
- "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ że to wywołuje `homeassistant.update_entity` co`interval`! 🔒",
+ "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒",
"detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów.",
"transition": "Transition time when applying a change to the lights (sekund)",
"transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙",
@@ -89,7 +89,7 @@
"description": "Światło(albo lista świateł), do których mają być zastosowane ustawieniam 💡"
},
"transition": {
- "description": "Czas tranzycji przy zmianie ustawień (w sekundach). 🕑"
+ "description": "Długość tranzycji do nowego stanu w sekundach. 🕑"
},
"adapt_color": {
"description": "Czy adaptować kolor światła"
@@ -142,6 +142,72 @@
},
"initial_transition": {
"description": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`(w sekundach). ⏲️"
+ },
+ "min_sunset_time": {
+ "description": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇"
+ },
+ "take_over_control": {
+ "description": "Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒"
+ },
+ "transition": {
+ "description": "Długość tranzycji do nowego stanu w sekundach. 🕑"
+ },
+ "autoreset_control_seconds": {
+ "description": "Czas po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️"
+ },
+ "adapt_delay": {
+ "description": "Czas (w sekundach) pomiędzy włączeniem światła a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Morze pomóc zredukować migotanie. ⏲️"
+ },
+ "max_color_temp": {
+ "description": "Najzimniejsza temperatura barwowa (w Kelwinach). ❄️"
+ },
+ "min_brightness": {
+ "description": "Minimalna jasność (w procentach). 💡"
+ },
+ "min_color_temp": {
+ "description": "Najcieplejsza temperatura barwowa (w Kelwinach). 🔥"
+ },
+ "only_once": {
+ "description": "Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄"
+ },
+ "prefer_rgb_color": {
+ "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła.. 🌈"
+ },
+ "separate_turn_on_commands": {
+ "description": "Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀"
+ },
+ "send_split_delay": {
+ "description": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️"
+ },
+ "sleep_brightness": {
+ "description": "Jasność świateł w trybie spania (w procentach). 😴"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "Użyj `\"rgb_color\"` albo `\"color_temp\"`trybie spania. 🌙"
+ },
+ "sleep_rgb_color": {
+ "description": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈"
+ },
+ "sleep_color_temp": {
+ "description": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴"
+ },
+ "sunrise_offset": {
+ "description": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰"
+ },
+ "sunrise_time": {
+ "description": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅"
+ },
+ "sunset_offset": {
+ "description": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰"
+ },
+ "sunset_time": {
+ "description": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇"
+ },
+ "max_sunrise_time": {
+ "description": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅"
+ },
+ "detect_non_ha_changes": {
+ "description": "Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów."
}
}
}
From 27d0c47d0a8bfd71cc3f02b3ad4aecfa1782f3ee Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Tue, 29 Aug 2023 04:20:26 +0200
Subject: [PATCH 148/549] Translated using Weblate (Dutch)
Currently translated at 69.2% (106 of 153 strings)
Co-authored-by: Hosted Weblate
Co-authored-by: Kees
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
Translation: Adaptive Lighting/Adaptive Lighting
---
custom_components/adaptive_lighting/translations/nl.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index 2a64b971..81030b6a 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -58,7 +58,8 @@
"sunset_offset": "Pas de tijd van zonsondergang aan met een positieve of negatieve verschuiving in seconden. ⏰",
"interval": "Frequentie om de lampen aan te passen, in seconden. 🔄",
"sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴",
- "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen."
+ "autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen.",
+ "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴"
}
}
},
From dd0b45f62330c07e77cf0b7a1b3914db9a406a6a Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Tue, 29 Aug 2023 04:20:26 +0200
Subject: [PATCH 149/549] Translated using Weblate (German)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently translated at 100.0% (153 of 153 strings)
Translated using Weblate (German)
Currently translated at 100.0% (153 of 153 strings)
Co-authored-by: Hosted Weblate
Co-authored-by: Mirco Hülsemann
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/de/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/de.json | 278 ++++++++++++++----
1 file changed, 216 insertions(+), 62 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/de.json b/custom_components/adaptive_lighting/translations/de.json
index 154d1fe0..acd2de22 100644
--- a/custom_components/adaptive_lighting/translations/de.json
+++ b/custom_components/adaptive_lighting/translations/de.json
@@ -1,67 +1,221 @@
{
- "title": "Adaptive Lighting",
- "config": {
- "step": {
- "user": {
- "title": "Benenne das Adaptive Lighting",
- "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten",
- "data": {
- "name": "Name"
- }
- }
- },
- "abort": {
- "already_configured": "Gerät ist bereits konfiguriert!"
+ "title": "Adaptive Beleuchtung",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Benenne die Adaptive Beleuchtung Instanz",
+ "description": "Jede Instanz kann mehrere Licht Entitäten beinhalten",
+ "data": {
+ "name": "Name"
}
+ }
},
- "options": {
- "step": {
- "init": {
- "title": "Adaptive Lighting Optionen",
- "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde.",
- "data": {
- "lights": "Lichter",
- "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt",
- "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)",
- "interval": "interval, Zeit zwischen Updates des Switches",
- "max_brightness": "max_brightness, maximale Helligkeit in %",
- "max_color_temp": "max_color_temp, maximale Farbtemperatur in Kelvin",
- "min_brightness": "min_brightness, minimale Helligkeit in %",
- "min_color_temp": "min_color_temp, minimale Farbtemperatur in Kelvin",
- "only_once": "only_once, passe die Lichter nur beim Einschalten an",
- "prefer_rgb_color": "prefer_rgb_color, nutze 'rgb_color' vor 'color_temp', wenn möglich",
- "separate_turn_on_commands": "separate_turn_on_commands, für jedes Attribut (Farbe, Helligkeit usw.) in 'light.turn_on' werden separate Befehle gesendet. Wird für manche Leuchtmittel benötigt.",
- "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.",
- "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %",
- "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'",
- "sleep_rgb_color": "sleep_rgb_color, in RGB",
- "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin",
- "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden",
- "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)",
- "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)",
- "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden",
- "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)",
- "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)",
- "take_over_control": "take_over_control, wenn irgendetwas während ein Licht an ist außer Adaptive Lighting den Service 'light.turn_on' aufruft, stoppe die Anpassung des Lichtes (oder des Schalters) bis dieser wieder von off -> on geschaltet wird.",
- "detect_non_ha_changes": "detect_non_ha_changes, entdeckt alle Änderungen über 10% am Licht (auch außerhalb von HA gemacht), 'take_over_control' muss aktiviert sein (ruft 'homeassistant.update_entity' jede 'interval' auf!)",
- "transition": "transition, Wechselzeit in Sekunden",
- "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.",
- "skip_redundant_commands": "Keine Adaptierungsbefehle senden, deren erwünschter Status schon dem bekanntes Status von Lichtern entspricht. Minimiert die Netzwerkbelastung und verbessert die Adaptierung in manchen Situationen. Deaktiviert lassen falls der pysikalische Status der Lichter und der erkannte Status in HA nicht synchron bleiben."
- }
- }
- },
- "error": {
- "option_error": "Fehlerhafte Option",
- "entity_missing": "Ein ausgewähltes Licht wurde nicht gefunden"
- }
- },
- "services": {
- "apply": {
- "fields": {
- "lights": {
- "description": "Ein Licht (oder eine Lichtliste), um die Einstellungen anzuwenden. RECHT"
- }
- }
- }
+ "abort": {
+ "already_configured": "Dieses Gerät ist bereits konfiguriert."
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Optionen für Adaptive Beleuchtung",
+ "description": "Alle Einstellungen für eine Adaptive Lighting Komponente. Die Optionsnamen entsprechen den YAML-Einstellungen. Es werden keine Optionen angezeigt, wenn dieser Eintrag in YAML konfiguriert wurde. Interaktive Diagramme zur Veranschaulichung der Auswirkungen der Parameter finden Sie unter [dieser Webanwendung](https://basnijholt.github.io/adaptive-lighting). Weitere Details finden Sie in der [offiziellen Dokumentation](https://github.com/basnijholt/adaptive-lighting#readme).",
+ "data": {
+ "lights": "Lichter",
+ "initial_transition": "initial_transition, wenn Lichter von 'off' zu 'on' wechseln oder wenn 'sleep_state' wechselt",
+ "sleep_transition": "sleep_transition: Wenn 'sleep_state' sich ändert. (Sekunden)",
+ "interval": "interval, Zeit zwischen Updates des Switches",
+ "max_brightness": "max_brightness: Maximale Helligkeit in Prozent. 💡",
+ "max_color_temp": "max_color_temp: Kälteste Farbtemperatur in Kelvin. ❄️",
+ "min_brightness": "min_brightness: Minimale Helligkeit in Prozent. 💡",
+ "min_color_temp": "min_color_temp: Wärmste Farbtemperatur in Kelvin. 🔥",
+ "only_once": "only_once: Lichter nur einmalig anpassen, wenn sie eingeschaltet werden (`true`) oder sie immer wieder anpassen (`false`). 🔄",
+ "prefer_rgb_color": "prefer_rgb_color: Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈",
+ "separate_turn_on_commands": "separate_turn_on_commands: Verwende getrennte `light.turn_on`-Aufrufe für Farbe und Helligkeit, erforderlich für einige Lichttypen. 🔀",
+ "send_split_delay": "send_split_delay: Wartezeit zwischen dem Senden der Befehle (Millisekunden), wenn separate_turn_on_commands genutzt wird. Kann helfen, wenn die Leuchtmittel die separaten Befehle nicht korrekt umsetzen.",
+ "sleep_brightness": "sleep_brightness, Schlafhelligkeit in %",
+ "sleep_rgb_or_color_temp": "sleep_rgb_or_color_temp, nutze 'rgb_color' oder 'color_temp'",
+ "sleep_rgb_color": "sleep_rgb_color, in RGB",
+ "sleep_color_temp": "sleep_color_temp, Schlaffarbtemperatur in Kelvin",
+ "sunrise_offset": "sunrise_offset, Sonnenaufgang Verschiebung in +/- Sekunden",
+ "sunrise_time": "sunrise_time, Sonnenaufgangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenaufgangs an deiner Position verwendet)",
+ "max_sunrise_time": "max_sunrise_time: Manuelles Überschreiben der max. sunrise_time. Falls 'None', wird die tatsächliche sunrise_time an deiner Position verwendet (HH:MM:SS)",
+ "sunset_offset": "sunset_offset, Sonnenuntergang Verschiebung in +/- Sekunden",
+ "sunset_time": "sunset_time, Sonnenuntergangszeit in 'HH:MM:SS' Format (wenn 'None' wird die aktuelle Zeit des Sonnenuntergangs an deiner Position verwendet)",
+ "min_sunset_time": "min_sunset_time: Manuelles Überschreiben der min. sunset_time. Falls 'None', wird die tatsächliche sunset_time an deiner Position verwendet (HH:MM:SS)",
+ "take_over_control": "take_over_control: Deaktiviere die adaptive Beleuchtung, wenn eine andere Quelle `light.turn_on` aufruft, während die Beleuchtung eingeschaltet ist und angepasst wird. Beachte, dass dies `homeassistant.update_entity` jedes `Intervall` aufruft! 🔒",
+ "detect_non_ha_changes": "detect_non_ha_changes: Erkennt und stoppt Anpassungen für nicht-`light.turn_on`-Zustandsänderungen. Benötigt, dass `take_over_control` aktiviert ist. 🕵️ Vorsicht: ⚠️ Einige Lichter können fälschlicherweise einen 'an'-Zustand anzeigen, was dazu führen kann, dass Lichter unerwartet eingeschaltet werden. Deaktiviere diese Funktion, wenn solche Probleme auftreten.",
+ "transition": "transition, Wechselzeit in Sekunden",
+ "adapt_delay": "adapt_delay: Wartezeit (in Sekunden) zwischen Anschalten des Licht und der Anpassung durch Adaptive Lights. Kann Flackern vermeiden.",
+ "skip_redundant_commands": "skip_redundant_commands: Überspringt das Senden von Anpassungsbefehlen, deren Zielzustand bereits mit dem bekannten Zustand der Leuchte übereinstimmt. Minimiert den Netzwerkverkehr und verbessert die Anpassungsreaktion in einigen Situationen. 📉 Deaktivieren, falls der physikalische Zustand der Lichter nicht mehr mit dem Zustand in HA übereinstimmt.",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Beim ersten Einschalten des Lichts. Wenn auf `true` gesetzt, passt AL das Licht nur an, wenn `light.turn_on` ohne eine Angabe von Farbe oder Helligkeit aufgerufen wird. ❌🌈 Dies verhindert z.B. die Anpassung durch AL beim Aktivieren einer Szene. Wenn auf \"false\" gesetzt, passt AL das licht unabhängig von der Angabe von Farbe oder Helligkeit in den ursprünglichen `service_data` an. Benötigt das `take_over_control` aktiviert ist. 🕵️ ",
+ "include_config_in_attributes": "include_config_in_attributes: Alle Optionen als Attribute auf dem Schalter im Home Assistant anzeigen, wenn auf `true` gesetzt. 📝",
+ "multi_light_intercept": "multi_light_intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, die auf mehrere Lichter aufrufen. ➗⚠️ Dies kann dazu führen, dass ein einzelner `light.turn_on`-Aufruf in mehrere Aufrufe aufgeteilt wird, z.B. wenn Lichter in verschiedenen Schaltern sind. Erfordert, dass `intercept` aktiviert ist.",
+ "transition_until_sleep": "transition_until_sleep: Wenn diese Option aktiviert ist, behandelt die adaptive Beleuchtung die Schlafeinstellungen als Minimum und geht nach Sonnenuntergang zu diesen Werten über. 🌙",
+ "intercept": "intercept: Abfangen und Anpassen von `light.turn_on`-Aufrufen, um eine sofortige Anpassung von Farbe und Helligkeit zu ermöglichen. 🏎️ Deaktivieren für Leuchten, die `light.turn_on` mit Farbe und Helligkeit nicht unterstützen."
+ },
+ "data_description": {
+ "sunrise_offset": "Anpassung der Sonnenaufgangszeit mit positivem oder negativem Versatz in Sekunden. ⏰",
+ "sunset_offset": "Anpassung der Sonnenuntergangszeit mit positivem oder negativem Versatz in Sekunden. ⏰",
+ "brightness_mode": "Helligkeitsmodus, der verwendet werden soll. Mögliche Werte sind `default`, `linear` und `tanh` (verwendet `brightness_mode_time_dark` und `brightness_mode_time_light`). 📈",
+ "send_split_delay": "Verzögerung (ms) zwischen `separate_turn_on_commands` für Leuchten, die keine gleichzeitige Einstellung von Helligkeit und Farbe unterstützen. ⏲️",
+ "transition": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑",
+ "sleep_rgb_color": "RGB-Farbe im Schlafmodus (wird verwendet, wenn `sleep_rgb_or_color_temp` `rgb_color` ist). 🌈",
+ "sunset_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenuntergang ein. 🌇",
+ "max_sunrise_time": "Lege die späteste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen früheren Sonnenaufgang zu ermöglichen. 🌅",
+ "min_sunset_time": "Lege die früheste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um spätere Sonnenuntergänge zu ermöglichen. 🌇",
+ "max_sunset_time": "Lege die späteste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um frühere Sonnenuntergänge zu ermöglichen. 🌇",
+ "adapt_delay": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️",
+ "min_sunrise_time": "Lege die früheste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen späteren Sonnenaufgang zu ermöglichen. 🌅",
+ "interval": "Häufigkeit der Lichtanpassung in Sekunden. 🔄",
+ "brightness_mode_time_light": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit nach/vor Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.",
+ "brightness_mode_time_dark": "(Wird ignoriert, wenn `brightness_mode='default'`) Die Dauer in Sekunden, um die Helligkeit vor/nach Sonnenaufgang/Sonnenuntergang hoch/runter zu fahren. 📈📉.",
+ "autoreset_control_seconds": "Setzt die manuelle Steuerung nach einer bestimmten Anzahl von Sekunden automatisch zurück. Zum Deaktivieren auf 0 setzen. ⏲️",
+ "sleep_brightness": "Helligkeit der Lichter im Schlafmodus in Prozent. 😴",
+ "sleep_color_temp": "Farbtemperatur im Schlafmodus in Kelvin (wird verwendet, wenn `sleep_rgb_or_color_temp` `color_temp` ist) . 😴",
+ "initial_transition": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️",
+ "sleep_rgb_or_color_temp": "Verwende entweder `rgb_color` oder `color_temp` im Schlafmodus. 🌙",
+ "sleep_transition": "Dauer des Übergangs, wenn der \"Schlafmodus\" umgeschaltet wird, in Sekunden. 😴",
+ "sunrise_time": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenaufgang ein. 🌅"
+ }
+ }
+ },
+ "error": {
+ "option_error": "Ungültige Option",
+ "entity_missing": "Ein oder mehrere ausgewählte Lichter fehlen in Home Assistant"
+ }
+ },
+ "services": {
+ "apply": {
+ "fields": {
+ "lights": {
+ "description": "Eine Leuchte (oder eine Liste von Leuchten), auf die die Einstellungen angewendet werden sollen. 💡"
+ },
+ "entity_id": {
+ "description": "Die `entity_id` des Schalters mit den zu übernehmenden Einstellungen. 📝"
+ },
+ "prefer_rgb_color": {
+ "description": "Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈"
+ },
+ "transition": {
+ "description": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑"
+ },
+ "adapt_brightness": {
+ "description": "Ob die Helligkeit des Lichts angepasst werden soll. 🌞"
+ },
+ "adapt_color": {
+ "description": "Ob die Farbtemperatur des Lichts angepasst werden soll. 🌈"
+ },
+ "turn_on_lights": {
+ "description": "Ob Lichter eingeschaltet werden sollen, die derzeit ausgeschaltet sind. 🔆"
+ }
+ },
+ "description": "Wendet die aktuellen Einstellungen der adaptiven Beleuchtung auf die Lichter an."
+ },
+ "change_switch_settings": {
+ "fields": {
+ "only_once": {
+ "description": "Lichter nur einmalig anpassen, wenn sie eingeschaltet werden (`true`) oder sie immer wieder anpassen (`false`). 🔄"
+ },
+ "detect_non_ha_changes": {
+ "description": "Erkennt und stoppt Anpassungen für nicht-`light.turn_on`-Zustandsänderungen. Benötigt, dass `take_over_control` aktiviert ist. 🕵️ Vorsicht: ⚠️ Einige Lichter können fälschlicherweise einen 'an'-Zustand anzeigen, was dazu führen kann, dass Lichter unerwartet eingeschaltet werden. Deaktiviere diese Funktion, wenn solche Probleme auftreten."
+ },
+ "min_brightness": {
+ "description": "Minimale Helligkeit in Prozent. 💡"
+ },
+ "sunset_time": {
+ "description": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenuntergang ein. 🌇"
+ },
+ "use_defaults": {
+ "description": "Setzt die nicht in diesem Service-Aufruf angegebenen Standardwerte. Optionen: `current` (Standard, behält die aktuellen Werte bei), `factory` (setzt auf die in der Dokumentation angegebenen Standardwerte zurück) oder `configuration` (setzt auf die Standardwerte der Switch-Konfiguration zurück). ⚙️"
+ },
+ "max_sunrise_time": {
+ "description": "Lege die späteste virtuelle Sonnenaufgangszeit (HH:MM:SS) fest, um einen früheren Sonnenaufgang zu ermöglichen. 🌅"
+ },
+ "include_config_in_attributes": {
+ "description": "Zeige alle Optionen als Attribute auf dem Schalter im Home Assistant, wenn auf `true` gesetzt. 📝"
+ },
+ "min_sunset_time": {
+ "description": "Lege die früheste virtuelle Sonnenuntergangszeit (HH:MM:SS) fest, um spätere Sonnenuntergänge zu ermöglichen. 🌇"
+ },
+ "sunrise_offset": {
+ "description": "Anpassung der Sonnenaufgangszeit mit positivem oder negativem Versatz in Sekunden. ⏰"
+ },
+ "sunset_offset": {
+ "description": "Anpassung der Sonnenuntergangszeit mit positivem oder negativem Versatz in Sekunden. ⏰"
+ },
+ "take_over_control": {
+ "description": "Deaktiviere die adaptive Beleuchtung, wenn eine andere Quelle `light.turn_on` aufruft, während die Beleuchtung eingeschaltet ist und angepasst wird. Beachte, dass dies `homeassistant.update_entity` jedes `Intervall` aufruft! 🔒"
+ },
+ "max_brightness": {
+ "description": "Maximale Helligkeit in Prozent. 💡"
+ },
+ "separate_turn_on_commands": {
+ "description": "Verwende getrennte `light.turn_on`-Aufrufe für Farbe und Helligkeit, erforderlich für einige Lichttypen. 🔀"
+ },
+ "sleep_brightness": {
+ "description": "Helligkeit der Lichter im Schlafmodus in Prozent. 😴"
+ },
+ "sleep_rgb_color": {
+ "description": "RGB-Farbe im Schlafmodus (wird verwendet, wenn `sleep_rgb_or_color_temp` `rgb_color` ist). 🌈"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "Verwende entweder `rgb_color` oder `color_temp` im Schlafmodus. 🌙"
+ },
+ "sleep_color_temp": {
+ "description": "Farbtemperatur im Schlafmodus in Kelvin (wird verwendet, wenn `sleep_rgb_or_color_temp` `color_temp` ist) . 😴"
+ },
+ "sunrise_time": {
+ "description": "Stelle eine feste Zeit (HH:MM:SS) für den Sonnenaufgang ein. 🌅"
+ },
+ "transition": {
+ "description": "Dauer des Übergangs beim Lichtwechsel in Sekunden. 🕑"
+ },
+ "adapt_delay": {
+ "description": "Wartezeit (Sekunden) zwischen dem Einschalten des Lichts und der Anwendung der adaptiven Beleuchtung. Könnte helfen, Flackern zu vermeiden. ⏲️"
+ },
+ "autoreset_control_seconds": {
+ "description": "Setzt die manuelle Steuerung nach einer bestimmten Anzahl von Sekunden automatisch zurück. Zum Deaktivieren auf 0 setzen. ⏲️"
+ },
+ "entity_id": {
+ "description": "Entity ID des Schalters. 📝"
+ },
+ "turn_on_lights": {
+ "description": "Ob Lichter eingeschaltet werden sollen, die derzeit ausgeschaltet sind. 🔆"
+ },
+ "initial_transition": {
+ "description": "Dauer des ersten Übergangs, wenn das Licht von `off` auf `on` schaltet, in Sekunden. ⏲️"
+ },
+ "sleep_transition": {
+ "description": "Dauer des Übergangs, wenn der \"Schlafmodus\" umgeschaltet wird, in Sekunden. 😴"
+ },
+ "max_color_temp": {
+ "description": "Kälteste Farbtemperatur in Kelvin. ❄️"
+ },
+ "min_color_temp": {
+ "description": "Wärmste Farbtemperatur in Kelvin. 🔥"
+ },
+ "send_split_delay": {
+ "description": "Verzögerung (ms) zwischen `separate_turn_on_commands` für Leuchten, die keine gleichzeitige Einstellung von Helligkeit und Farbe unterstützen. ⏲️"
+ },
+ "prefer_rgb_color": {
+ "description": "Ob die RGB-Farbanpassung der Farbtemperaturanpassung vorgezogen werden soll, wenn möglich. 🌈"
+ }
+ },
+ "description": "Ändern Sie alle Einstellungen, die Sie im Schalter wünschen. Alle Optionen hier sind die gleichen wie im config Flow."
+ },
+ "set_manual_control": {
+ "fields": {
+ "manual_control": {
+ "description": "Ob das Licht aus der Liste `manual_control` hinzugefügt (`true`) oder entfernt (`false`) werden soll. 🔒"
+ },
+ "lights": {
+ "description": "entity_id(s) der Lichter, wenn nichts angegeben wird, werden alle Lichter des Schalters ausgewählt. 💡"
+ },
+ "entity_id": {
+ "description": "Die `entity_id` des Schalters, in dem das Licht als `manuell gesteuert` (un)markiert werden soll. 📝"
+ }
+ },
+ "description": "Markiere, ob ein Licht \"manuell gesteuert\" ist."
+ }
+ }
}
From e8cbc6a8d2b07df5ccf60067a20c5f416824b192 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Tue, 29 Aug 2023 04:20:26 +0200
Subject: [PATCH 150/549] Translated using Weblate (Spanish)
Currently translated at 100.0% (153 of 153 strings)
Translated using Weblate (Spanish)
Currently translated at 47.0% (72 of 153 strings)
Added translation using Weblate (Spanish)
Co-authored-by: Fernando Belaza
Co-authored-by: Gerard Rubio
Co-authored-by: Hosted Weblate
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/es/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/es.json | 202 ++++++++++++++++++
1 file changed, 202 insertions(+)
create mode 100644 custom_components/adaptive_lighting/translations/es.json
diff --git a/custom_components/adaptive_lighting/translations/es.json b/custom_components/adaptive_lighting/translations/es.json
new file mode 100644
index 00000000..c90b842e
--- /dev/null
+++ b/custom_components/adaptive_lighting/translations/es.json
@@ -0,0 +1,202 @@
+{
+ "title": "Iluminación Adaptativa",
+ "options": {
+ "step": {
+ "init": {
+ "title": "Configuración de la Iluminación Adaptativa",
+ "data_description": {
+ "sunset_offset": "Define la hora de la puesta del sol con un desfase (positivo o negativo) en segundos. ⏰",
+ "sunrise_offset": "Define la hora de la salida del sol con un desfase (positivo o negativo) en segundos. ⏰",
+ "sleep_color_temp": "Temperatura de color en modo noche (usado cuando`sleep_rgb_or_color_temp` es `color_temp`) en grados Kelvin. 😴",
+ "send_split_delay": "Retraso (ms) entre `separate_turn_on_commands` para luces que no soportan ajustes simultáneos de brillo y color. ⏲️",
+ "transition": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️",
+ "initial_transition": "Duración de la primera transición cuando las luces pasan de `off` a `on` en segundos. ⏲️",
+ "sleep_transition": "Duración de la transición cuando el \"modo noche\" se activa o desactiva, en segundos. 😴",
+ "max_sunrise_time": "Define el amanecer virtual más tardío (HH:MM:SS), permitiendo amaneceres más tempranos. 🌅",
+ "max_sunset_time": "Define el atardecer virtual más tardío (HH:MM:SS), permitiendo atardeceres más tempranos. 🌇",
+ "sleep_brightness": "Porcentaje de brillo de las luces en el modo noche. 😴",
+ "interval": "Frecuencia de adaptación de las luces, en segundos. 🔄",
+ "sleep_rgb_color": "Color RGB en modo noche(usado cuando `sleep_rgb_or_color_temp` es \"rgb_color\"). 🌈",
+ "sunrise_time": "Fijar una hora (HH:MM:SS) para el amanecer. 🌅",
+ "min_sunrise_time": "Define el amanecer virtual más temprano (HH:MM:SS), permitiendo amaneceres más tardíos. 🌅",
+ "sleep_rgb_or_color_temp": "Usar el modo`\"rgb_color\"` o `\"color_temp\"` en el modo noche. 🌙",
+ "autoreset_control_seconds": "Resetear automáticamente el control manual tras `X` segundos. Poner a 0 para deshabilitar.",
+ "brightness_mode": "Modo de brillo a usar. Valores posibles son: `default`, `linear` y `tanh` (usa`brightness_mode_time_dark` y `brightness_mode_time_light`). 📈",
+ "brightness_mode_time_light": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.",
+ "brightness_mode_time_dark": "(Ignorado si `brightness_mode='default'`) La duración, en segundos, de la transición del brillo después/antes del amanecer/atardecer. 📈📉.",
+ "sunset_time": "Fijar una hora (HH:MM:SS) para el atardecer. 🌇",
+ "min_sunset_time": "Define el atardecer virtual más temprano (HH:MM:SS), permitiendo atardeceres más tardíos. 🌇",
+ "adapt_delay": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️"
+ },
+ "data": {
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Al encender las luces. Si el valor es `true`, AL adapta sólo si se llama `light.turn_on` sin especificar el color o brillo. ❌🌈 Esto, por ejemplo, previene la adaptación al activar una escena. Si el valor es `false`, AL adapta independientemente de la presencia de color o brillo en `service_data` inicial. Necesita `take_over_control` habilitado. 🕵️ ",
+ "detect_non_ha_changes": "detect_non_ha_changes: Detecta e interrumpe adaptaciones para cambios de estado no `light.turn_on`. Necesita `take_over_control` habilitado. 🕵️ Precaución: ⚠️ Algunas luces pueden indicar de forma errónea un estado 'on', que puede resultar en luces que se enciendan de forma no esperada. Deshabilita esta función si encuentras dichos problemas.",
+ "intercept": "intercept: Intercepta y adapta llamadas a `light.turn_on` para habilitar adaptaciones instantáneas de color y brillo. 🏎️ Deshabilitar para luces que no soporten `light.turn_on` con color y brillo.",
+ "min_color_temp": "min_color_temp: Temperatura de color más cálida en grados Kelvin. 🔥",
+ "lights": "lights: Lista de entity_ids de luces a controlar (puede estar vacía). 🌟",
+ "max_brightness": "max_brightness: Porcentaje máximo de brillo. 💡",
+ "max_color_temp": "max_color_temp: Temperatura de color más fría en grados Kelvin. ❄️",
+ "min_brightness": "min_brightness: Porcentaje mínimo de brillo. 💡",
+ "prefer_rgb_color": "prefer_rgb_color: Preferir ajustar el color RGB a la temperatura de color cuando sea posible. 🌈",
+ "transition_until_sleep": "transition_until_sleep: Cuando habilitado, Adaptive Lighting tratará los ajustes del modo noche como los valores mínimos, transicionando a esos valores tras la puesta del sol. 🌙",
+ "include_config_in_attributes": "include_config_in_attributes: Muestra todas las opciones como atributes del interruptor en Home Assistant cuando sea `true`. 📝",
+ "multi_light_intercept": "multi_light_intercept: Intercepta y adapta llamadas a `light.turn_on` que apuntan a múltiples luces. ➗⚠️ Esto puede resultar en dividir una única llamada a `light.turn_on` en múltiples llamadas, por ejemplo, cuando las luces están vinculadas a distintos interruptores. Requiere que `intercept` esté habilitado.",
+ "only_once": "only_once: Adapta las luces sólo cuando se encienden (`true`) o mantener adaptadas (`false`). 🔄",
+ "separate_turn_on_commands": "separate_turn_on_commands: Usar llamadas independientes a `light.turn_on` para color y brillo, necesario para ciertos tipos de luces. 🔀",
+ "skip_redundant_commands": "skip_redundant_commands: Evitar mandar comandos de adaptación a luces cuyo estado ya sea el esperado. Reduce tráfico en la red y mejora la respuesta de la adaptación en ciertas situaciones. 📉Deshabilitar si el estado real de las luces se desincroniza con el estado registrado en Home Assistant.",
+ "take_over_control": "take_over_control: Deshabilita Adaptive Lighting si otra fuente llama`light.turn_on` mientras las luces están encendidas y adaptándose. Cuidado, esto llama`homeassistant.update_entity` cada `interval`! 🔒"
+ },
+ "description": "Configura un componente Adaptive Lighting. Los nombres de las opciones se asemejan a las disponibles en la configuración YAML. Si has definido esta entrada en YAML, no aparecerá ninguna opción aquí. Para gráficos interactivos que demuestran los efectos de los parámetros, visita [esta web app](https://basnijholt.github.io/adaptive-lighting). Para más detalles, ver la [documentación oficial](https://github.com/basnijholt/adaptive-lighting#readme)."
+ }
+ },
+ "error": {
+ "option_error": "Opción no válida",
+ "entity_missing": "Una o más entidades de luz seleccionadas no se encuentran en Home Assistant"
+ }
+ },
+ "services": {
+ "apply": {
+ "fields": {
+ "lights": {
+ "description": "Luz (o listado de luces) sobre las que aplicar la configuración. 💡"
+ },
+ "transition": {
+ "description": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️"
+ },
+ "adapt_color": {
+ "description": "Adaptar (o no) el color en luces que lo soporten. 🌈"
+ },
+ "prefer_rgb_color": {
+ "description": "Preferir ajustes de color RGB a temperatura de color cuando sea posible. 🌈"
+ },
+ "turn_on_lights": {
+ "description": "Encender (o no) luces que estén apagadas. 🔆"
+ },
+ "entity_id": {
+ "description": "El `entity_id` del interruptor con los ajustes a aplicar. 📝"
+ },
+ "adapt_brightness": {
+ "description": "Adaptar (o no) el brillo de la luz. 🌞"
+ }
+ },
+ "description": "Aplica la configuración actual de Adaptive Lighting a las luces."
+ },
+ "change_switch_settings": {
+ "fields": {
+ "sunrise_offset": {
+ "description": "Define la hora de la salida del sol con un desfase (positivo o negativo) en segundos. ⏰"
+ },
+ "sunset_offset": {
+ "description": "Define la hora de la puesta del sol con un desfase (positivo o negativo) en segundos. ⏰"
+ },
+ "only_once": {
+ "description": "Adaptar las luces solo cuando se enciendan (`true`) o hacerlo siempre (`false`). 🔄"
+ },
+ "sleep_color_temp": {
+ "description": "Temperatura de color en modo noche (usado cuando`sleep_rgb_or_color_temp` es `color_temp`) en grados Kelvin. 😴"
+ },
+ "max_color_temp": {
+ "description": "Temperatura de color más fría en grados Kelvin. ❄️"
+ },
+ "send_split_delay": {
+ "description": "Retraso (ms) entre `separate_turn_on_commands` para luces que no soportan ajustes simultáneos de brillo y color. ⏲️"
+ },
+ "detect_non_ha_changes": {
+ "description": "detect_non_ha_changes: Detecta e interrumpe adaptaciones para cambios de estado no `light.turn_on`. Necesita `take_over_control` habilitado. 🕵️ Precaución: ⚠️ Algunas luces pueden indicar de forma errónea un estado 'on', que puede resultar en luces que se enciendan de forma no esperada. Deshabilita esta función si encuentras dichos problemas."
+ },
+ "take_over_control": {
+ "description": "Deshabilita Adaptive Lighting si otra fuente llama `light.turn_on` mientras las luces se estan adaptando. Cuidado porque esto llama a `homeassistant.update_entity` cada`interval`! 🔒"
+ },
+ "initial_transition": {
+ "description": "Duración de la primera transición cuando las luces pasan de `off` a `on` en segundos. ⏲️"
+ },
+ "transition": {
+ "description": "Duración de la transición cuando las luces se adaptan, en segundos. ⏲️"
+ },
+ "entity_id": {
+ "description": "ID de la entidad del interruptor. 📝"
+ },
+ "sleep_transition": {
+ "description": "Duración de la transición cuando el \"modo noche\" se activa o desactiva, en segundos. 😴"
+ },
+ "min_brightness": {
+ "description": "Porcentaje mínimo de brillo. 💡"
+ },
+ "include_config_in_attributes": {
+ "description": "Muestra todas las opciones como atributos del interruptor en Home Assistant cuando sea `true`. 📝"
+ },
+ "prefer_rgb_color": {
+ "description": "Preferir ajustes de color RGB a temperatura de color cuando sea posible. 🌈"
+ },
+ "turn_on_lights": {
+ "description": "Encender (o no) luces que estén apagadas. 🔆"
+ },
+ "max_brightness": {
+ "description": "Porcentaje máximo de brillo. 💡"
+ },
+ "use_defaults": {
+ "description": "Define los valores por defecto no especificados en la llamada al servicio. Opciones: \"current\" (predeterminado, mantiene los valores actuales), \"factory\" (resetea a los valores documentados predeterminados), o \"configuration\" (revierte a los valores por defecto del interruptor). ⚙️"
+ },
+ "separate_turn_on_commands": {
+ "description": "Usar llamadas independientes a`light.turn_on` para color y brillo, necesario para cierto tipo de luces. 🔀"
+ },
+ "min_color_temp": {
+ "description": "Temperatura de color más cálida en grados Kelvin. 🔥"
+ },
+ "autoreset_control_seconds": {
+ "description": "Resetear automáticamente el control manual tras `x` segundos. Poner a 0 para deshabilitar. ⏲️"
+ },
+ "sleep_brightness": {
+ "description": "Porcentaje de brillo en el modo noche. 😴"
+ },
+ "sleep_rgb_color": {
+ "description": "Color RGB en modo noche(usado cuando `sleep_rgb_or_color_temp` es \"rgb_color\"). 🌈"
+ },
+ "sunrise_time": {
+ "description": "Fijar una hora (HH:MM:SS) para el amanecer. 🌅"
+ },
+ "sunset_time": {
+ "description": "Fijar una hora (HH:MM:SS) para el atardecer. 🌇"
+ },
+ "min_sunset_time": {
+ "description": "Define el atardecer virtual más temprano (HH:MM:SS), permitiendo atardeceres más tardíos. 🌇"
+ },
+ "max_sunrise_time": {
+ "description": "Define el amanecer virtual más tardío (HH:MM:SS), permitiendo amaneceres más tempranos. 🌅"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "Usar el modo`\"rgb_color\"` o `\"color_temp\"` en el modo noche. 🌙"
+ },
+ "adapt_delay": {
+ "description": "Tiempo de espera (segundos) entre el encendido de la luz y Adaptive Lighting aplicando cambios. Puede ayudar a evitar parpadeos. ⏲️"
+ }
+ },
+ "description": "Modifica cualquier ajuste que quieras en el interruptor. Todas las opciones aquí presentes son idénticas a la configuración del flujo."
+ },
+ "set_manual_control": {
+ "fields": {
+ "manual_control": {
+ "description": "Añadir (\"true\") o quitar (\"false\") la luz de la lista de `manual_control`. 🔒"
+ },
+ "lights": {
+ "description": "entity_id(s) de las luces, si no se especifica, se seleccionaran todas las luces vinculadas al interruptor. 💡"
+ },
+ "entity_id": {
+ "description": "El `entity_id` del interruptor en el cual (des)marcar la luz como estando `manually controlled`. 📝"
+ }
+ },
+ "description": "Señala si una luz está 'controlada manualmente'."
+ }
+ },
+ "config": {
+ "step": {
+ "user": {
+ "title": "Elige un nombre para la instancia de Adaptive Lighting",
+ "description": "Cada instancia puede contener múltiples luces!"
+ }
+ },
+ "abort": {
+ "already_configured": "El dispositivo ya está configurado"
+ }
+ }
+}
From e332b225f8ed71ad4f9b4244b2e27d5105521ddf Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Tue, 29 Aug 2023 02:24:22 +0000
Subject: [PATCH 151/549] docs: update README.md
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index f8386453..cb261607 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -529,6 +529,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 lukerix 🌍 |
 Maxime Bailleul 🌍 |
 Michel Balzer 🌍 |
+  Enrico Gambini 🌍 |
From 5144fab7a3c94d7ed566291a9809a72626aee7d2 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Tue, 29 Aug 2023 02:24:23 +0000
Subject: [PATCH 152/549] docs: update .all-contributorsrc
---
.all-contributorsrc | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index ae9114e2..da89bbe4 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -485,6 +485,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "enrico1036",
+ "name": "Enrico Gambini",
+ "avatar_url": "https://avatars.githubusercontent.com/u/9280405?v=4",
+ "profile": "https://github.com/enrico1036",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
From f7f3c8fd25bf52606a5495d8dace4a4734ca95d0 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Mon, 28 Aug 2023 19:25:06 -0700
Subject: [PATCH 153/549] docs: add MirCore as a contributor for translation
(#777)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index da89bbe4..cc77e287 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -494,6 +494,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "MirCore",
+ "name": "MirCore",
+ "avatar_url": "https://avatars.githubusercontent.com/u/9919366?v=4",
+ "profile": "https://github.com/MirCore",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index cb261607..72b318ba 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -530,6 +530,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Maxime Bailleul 🌍 |
 Michel Balzer 🌍 |
 Enrico Gambini 🌍 |
+  MirCore 🌍 |
From f3044efca9fda5076a50035cb62ffc2536bb780d Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Mon, 28 Aug 2023 19:25:32 -0700
Subject: [PATCH 154/549] docs: add KilFer as a contributor for translation
(#778)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index cc77e287..5bd4f818 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -503,6 +503,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "KilFer",
+ "name": "Fernando Belaza",
+ "avatar_url": "https://avatars.githubusercontent.com/u/290854?v=4",
+ "profile": "http://kilfer.es/",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 72b318ba..792540fe 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -531,6 +531,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Michel Balzer 🌍 |
 Enrico Gambini 🌍 |
 MirCore 🌍 |
+  Fernando Belaza 🌍 |
From 570c5cde2bf63dbdcfd778a55df8c6ac3c36a878 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 30 Aug 2023 20:42:06 -0700
Subject: [PATCH 155/549] docs: add wilcomir as a contributor for translation
(#781)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 5bd4f818..3ff26034 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -512,6 +512,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "wilcomir",
+ "name": "Vladimir Cravero",
+ "avatar_url": "https://avatars.githubusercontent.com/u/795981?v=4",
+ "profile": "https://github.com/wilcomir",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 792540fe..3144f254 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -532,6 +532,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Enrico Gambini 🌍 |
 MirCore 🌍 |
 Fernando Belaza 🌍 |
+  Vladimir Cravero 🌍 |
From 7a19ac7ace0c6b004532281542f89c7dd6a63ed8 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 30 Aug 2023 20:47:07 -0700
Subject: [PATCH 156/549] docs: add letroll as a contributor for translation
(#782)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 5 ++++-
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 3ff26034..1d853b07 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -521,6 +521,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "letroll",
+ "name": "Julien Quiévreux",
+ "avatar_url": "https://avatars.githubusercontent.com/u/255774?v=4",
+ "profile": "http://www.latavernedutroll.fr",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 3144f254..909e36d2 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -534,6 +534,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Fernando Belaza 🌍 |
 Vladimir Cravero 🌍 |
+
+  Julien Quiévreux 🌍 |
+
From c0d28ab4c572a2283a79f79e42c239467ed81bc6 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Wed, 30 Aug 2023 20:54:18 +0200
Subject: [PATCH 157/549] Translated using Weblate (Italian)
Currently translated at 58.1% (89 of 153 strings)
Co-authored-by: Vladimir Cravero
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/it/
Translation: Adaptive Lighting/Adaptive Lighting
---
custom_components/adaptive_lighting/translations/it.json | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json
index c1dccbbe..3f8c19e8 100644
--- a/custom_components/adaptive_lighting/translations/it.json
+++ b/custom_components/adaptive_lighting/translations/it.json
@@ -41,10 +41,12 @@
"detect_non_ha_changes": "detect_non_ha_changes: rileva tutti i cambiamenti >10% applicati alle luci (anche fuori da HA), richiede che 'take_over_control' sia abilitato (chiama 'homeassistant.update_entity' ad ogni 'intervallo'!)",
"transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)",
"adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii.",
- "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙"
+ "transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quando accendi le luci la prima volta. Se impostato su `true`, AL adatta solo se `light.turn_on è invocato senza specificare il colore o la luminosità. ❌🌈 Questo, per esempio, previene l'adattamento quando si attiva una scena. Se `false`, AL adatta indipendentemente dalla presenza di colore o luminosità nei `service_data` iniziali. Necessita che `take_over_control` sia abilitato. 🕵️ "
},
"data_description": {
- "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰"
+ "sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰",
+ "sunset_offset": "Modifica l'orario del tramonto con un offset in secondi positivo o negativo. ⏰"
}
}
},
@@ -58,6 +60,9 @@
"fields": {
"only_once": {
"description": "Adatta le luci solo nel momento in cui vengono accese ('true') o continua ad adattarle ('false'). 🔄"
+ },
+ "sunrise_offset": {
+ "description": "Modifica l'orario dell'alba con un offset in secondi positivo o negativo."
}
}
}
From f085c47b6376fbb454eac285301715ea116dff23 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Wed, 30 Aug 2023 20:54:18 +0200
Subject: [PATCH 158/549] Translated using Weblate (French)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Currently translated at 73.2% (112 of 153 strings)
Co-authored-by: Julien Quiévreux
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/fr/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/fr.json | 20 ++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/translations/fr.json b/custom_components/adaptive_lighting/translations/fr.json
index e9bb3710..26728081 100644
--- a/custom_components/adaptive_lighting/translations/fr.json
+++ b/custom_components/adaptive_lighting/translations/fr.json
@@ -50,7 +50,10 @@
"brightness_mode": "Mode de luminosité à utiliser. Les valeurs possibles sont < < par défaut > > , < < linéaire > > et < < parois > > , < < par défaut > > , et < < par coup > > , < < par défaut > > et par > par > . 📈",
"send_split_delay": "Délai (ms) entre \"separate_turn_on_commands\" pour les lumières qui ne supportent pas la commande de luminosité et le réglage de couleur en même temps. ⏲",
"sleep_color_temp": "Température de couleur en mode sommeil (utilisée lorsque `sleep_rgb_or_color_temp` est `color_temp`) en Kelvin. 😴",
- "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰"
+ "sunrise_offset": "Ajuster l'heure du lever de soleil avec un décalage positif ou négatif en secondes. ⏰",
+ "transition": "Durée de la transition des changements lumineux, en secondes. 🕑",
+ "initial_transition": "Durée de la première transition des lampes passant de `off` à `on` en secondes. ⏲️",
+ "sleep_transition": "Durée de la transition quand le \"mode sommeil\" est déclenché en secondes. 😴"
}
}
},
@@ -88,6 +91,18 @@
},
"sleep_color_temp": {
"description": "Température de couleur en mode sommeil (utilisé lorsque \"sleep_rgb_or_color_temp\" est défini sur \"color_temp\") en Kelvin. 😴"
+ },
+ "entity_id": {
+ "description": "ID de l'Entité de l'interrupteur. 📝"
+ },
+ "initial_transition": {
+ "description": "Durée de la première transition des lampes passant de `off` à `on` en secondes. ⏲️"
+ },
+ "transition": {
+ "description": "Durée de la transition des changements lumineux, en secondes. 🕑"
+ },
+ "sleep_transition": {
+ "description": "Durée de la transition quand le \"mode sommeil\" est déclenché en secondes. 😴"
}
},
"description": "Changez les réglages que vous souhaitez dans le commutateur. Toutes les options ici sont les mêmes que dans le flux de configuration."
@@ -97,6 +112,9 @@
"fields": {
"lights": {
"description": "Une lumière (ou une liste de lumières) pour appliquer les réglages. personnalisation"
+ },
+ "transition": {
+ "description": "Durée de la transition des changements lumineux, en secondes. 🕑"
}
}
}
From c978c43f38dd18f6c98029fb8930833af999ff09 Mon Sep 17 00:00:00 2001
From: lightrabbit
Date: Fri, 1 Sep 2023 05:57:42 +0800
Subject: [PATCH 159/549] Add Simplified Chinese translation (#775)
* Add Simplified Chinese translation
* Update zh-Hans.json
Remove unexisted word and fix some titles.
---
.../translations/zh-Hans.json | 269 ++++++++++++++++++
1 file changed, 269 insertions(+)
create mode 100644 custom_components/adaptive_lighting/translations/zh-Hans.json
diff --git a/custom_components/adaptive_lighting/translations/zh-Hans.json b/custom_components/adaptive_lighting/translations/zh-Hans.json
new file mode 100644
index 00000000..e9b206c6
--- /dev/null
+++ b/custom_components/adaptive_lighting/translations/zh-Hans.json
@@ -0,0 +1,269 @@
+{
+ "title": "自适应照明",
+ "config": {
+ "step": {
+ "user": {
+ "title": "为自适应照明实例选择一个名称",
+ "description": "每个实例可以包含多个灯光!",
+ "data": {
+ "name": "名称"
+ }
+ }
+ },
+ "abort": {
+ "already_configured": "此设备已配置"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "自适应照明选项",
+ "description": "配置自适应照明组件。选项名称与YAML设置对齐。如果在YAML中定义了此条目,则此处不会显示任何选项。有关演示参数影响的交互式图表,请访问[此Web应用程序](https://basnijholt.github.io/adaptive-lighting)。有关更多详细信息,请参阅[官方文档](https://github.com/basnijholt/adaptive-lighting#readme)。",
+ "data": {
+ "lights": "lights:要控制的灯光实体ID列表(可以为空)。🌟",
+ "interval": "频率(interval)",
+ "transition": "过渡(transition)",
+ "initial_transition": "初始过渡(initial_transition)",
+ "min_brightness": "min_brightness:最小亮度百分比。💡",
+ "max_brightness": "max_brightness:最大亮度百分比。💡",
+ "min_color_temp": "min_color_temp:最暖的色温,以开尔文为单位。🔥",
+ "max_color_temp": "max_color_temp:最冷的色温,以开尔文为单位。❄️",
+ "prefer_rgb_color": "prefer_rgb_color:在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈",
+ "sleep_brightness": "睡眠模式亮度(sleep_brightness)",
+ "sleep_rgb_or_color_temp": "睡眠模式RGB或色温(sleep_rgb_or_color_temp)",
+ "sleep_color_temp": "睡眠模式中的色温(sleep_color_temp)",
+ "sleep_rgb_color": "睡眠模式中的RGB颜色(sleep_rgb_color)",
+ "sleep_transition": "睡眠模式过渡时间(sleep_transition)",
+ "transition_until_sleep": "transition_until_sleep:启用时,自适应照明将将睡眠设置视为最小值,在日落后过渡到这些值。🌙",
+ "sunrise_time": "日出时间(sunrise_time)",
+ "min_sunrise_time": "最早日出时间(min_sunrise_time)",
+ "max_sunrise_time": "最晚日出时间(max_sunrise_time)",
+ "sunrise_offset": "日出时间偏移(sunrise_offset)",
+ "sunset_time": "日落时间(sunset_time)",
+ "min_sunset_time": "最早日落时间(min_sunset_time)",
+ "max_sunset_time": "最晚日落时间(max_sunset_time)",
+ "sunset_offset": "日落时间偏移(sunset_offset)",
+ "brightness_mode": "亮度模式(brightness_mode)",
+ "brightness_mode_time_dark": "变暗时间(brightness_mode_time_dark)",
+ "brightness_mode_time_light": "变亮时间(brightness_mode_time_light)",
+ "take_over_control": "take_over_control: 如果在灯光处于开启并处于适应照明的状态时,另一个来源调用`light.turn_on`,则禁用自适应照明。请注意,这会在每个`interval`调用`homeassistant.update_entity`!🔒",
+ "detect_non_ha_changes": "detect_non_ha_changes: 检测非`light.turn_on`的状态更改,并停止自适应照明。需要启用`take_over_control`。🕵️ 注意:⚠️ 一些灯光可能错误地显示为“开启”状态,这可能会导致灯光意外打开。如果遇到此类问题,请禁用此功能。",
+ "autoreset_control_seconds": "自动重置时间(autoreset_control_seconds)",
+ "only_once": "only_once:仅在打开时调整灯光(`true`)或始终调整灯光(`false`)。🔄",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on:当首次打开灯光时。如果设置为`true`,仅在没有指定颜色或亮度的情况下,AL才进行适应。❌🌈 例如,这可以防止在激活场景时进行适应。如果为`false`,则不考虑初始`service_data`中是否存在颜色或亮度,AL都会适应。需要启用`take_over_control`。🕵️ ",
+ "separate_turn_on_commands": "separate_turn_on_commands:为某些灯光类型需要使用单独的`light.turn_on`调用来设置颜色和亮度。🔀",
+ "send_split_delay": "指令发送间隔延迟(send_split_delay)",
+ "adapt_delay": "自适应照明延迟(adapt_delay)",
+ "skip_redundant_commands": "skip_redundant_commands:跳过目标状态已经等于灯光已知状态的自适应命令。在某些情况下,可以减少网络流量并提高适应响应性。📉如果物理灯光状态与HA的记录状态不同步,请禁用此功能。",
+ "intercept": "intercept:拦截并适应`light.turn_on`调用,以实现即时的颜色和亮度适应。🏎️ 对于不支持使用颜色和亮度进行`light.turn_on`的灯光,禁用此功能。",
+ "multi_light_intercept": "multi_light_intercept:拦截和适应针对多个灯光的`light.turn_on`调用。➗⚠️ 这可能会将单个`light.turn_on`调用拆分为多个调用,例如当灯光位于不同的开关中时。需要启用`intercept`。",
+ "include_config_in_attributes": "include_config_in_attributes:在Home Assistant中将所有选项显示为开关的属性时,设置为`true`。📝"
+ },
+ "data_description": {
+ "interval": "调整灯光的频率,以秒为单位。🔄",
+ "transition": "灯光变化时的过渡持续时间,以秒为单位。🕑",
+ "initial_transition": "灯光从“关闭”到“开启”时的第一个过渡持续时间,以秒为单位。⏲️",
+ "sleep_brightness": "睡眠模式中的亮度百分比。😴",
+ "sleep_rgb_or_color_temp": "在睡眠模式中使用“rgb_color”或“color_temp”。🌙",
+ "sleep_color_temp": "睡眠模式中的色温(当`sleep_rgb_or_color_temp`为`color_temp`时使用),以开尔文为单位。😴",
+ "sleep_rgb_color": "睡眠模式中的RGB颜色(当`sleep_rgb_or_color_temp`为“rgb_color”时使用)。🌈",
+ "sleep_transition": "切换“睡眠模式”时的过渡持续时间,以秒为单位。😴",
+ "sunrise_time": "设置固定的日出时间(HH:MM:SS)。🌅",
+ "min_sunrise_time": "设置最早的虚拟日出时间(HH:MM:SS),允许更晚的日出。🌅",
+ "max_sunrise_time": "设置最晚的虚拟日出时间(HH:MM:SS),允许更早的日出。🌅",
+ "sunrise_offset": "以秒为单位的正负偏移调整日出时间。⏰",
+ "sunset_time": "设置固定的日落时间(HH:MM:SS)。🌇",
+ "min_sunset_time": "设置最早的虚拟日落时间(HH:MM:SS),允许更晚的日落。🌇",
+ "max_sunset_time": "设置最晚的虚拟日落时间(HH:MM:SS),允许更早的日落。🌇",
+ "sunset_offset": "以秒为单位的正负偏移调整日落时间。⏰",
+ "brightness_mode": "要使用的亮度模式。可能的值为`default`、`linear`和`tanh`(使用`brightness_mode_time_dark`和`brightness_mode_time_light`)。📈",
+ "brightness_mode_time_dark": "(如果`brightness_mode='default'`将被忽略)日出/日落之前/之后亮度逐渐增加/减少的持续时间,以秒为单位。📈📉",
+ "brightness_mode_time_light": "(如果`brightness_mode='default'`将被忽略)日出/日落之后/之前亮度逐渐增加/减少的持续时间,以秒为单位。📈📉。",
+ "autoreset_control_seconds": "在若干秒后自动重置手动控制。设置为0以禁用。⏲️",
+ "send_split_delay": "对于不支持同时设置亮度和颜色的灯光,`separate_turn_on_commands`之间的延迟时间(毫秒)。⏲️",
+ "adapt_delay": "灯光打开和自适应照明应用更改之间的等待时间(秒)。可能有助于避免闪烁。⏲️"
+ }
+ }
+ },
+ "error": {
+ "option_error": "无效的选项",
+ "entity_missing": "一个或多个选择的灯光实体在Home Assistant中不存在"
+ }
+ },
+ "services": {
+ "apply": {
+ "name": "应用",
+ "description": "将当前自适应照明设置应用于灯光。",
+ "fields": {
+ "entity_id": {
+ "description": "具有要应用设置的开关的`entity_id`。📝",
+ "name": "entity_id"
+ },
+ "lights": {
+ "description": "要应用设置的灯光(或灯光列表)。💡",
+ "name": "lights"
+ },
+ "transition": {
+ "description": "灯光变化时的过渡持续时间,以秒为单位。🕑",
+ "name": "transition"
+ },
+ "adapt_brightness": {
+ "description": "是否调整灯光的亮度。🌞",
+ "name": "adapt_brightness"
+ },
+ "adapt_color": {
+ "description": "是否在支持的灯光上调整颜色。🌈",
+ "name": "adapt_color"
+ },
+ "prefer_rgb_color": {
+ "description": "在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈",
+ "name": "prefer_rgb_color"
+ },
+ "turn_on_lights": {
+ "description": "是否打开当前关闭的灯光。🔆",
+ "name": "turn_on_lights"
+ }
+ }
+ },
+ "set_manual_control": {
+ "name": "设置手动控制",
+ "description": "标记灯光是否为'手动控制'。",
+ "fields": {
+ "entity_id": {
+ "description": "要在其中(取消)标记灯光为“手动控制”的开关的`entity_id`。📝",
+ "name": "entity_id"
+ },
+ "lights": {
+ "description": "如果未指定,则为灯光的entity_id(s),如果未指定,则选择开关中的所有灯光。💡",
+ "name": "lights"
+ },
+ "manual_control": {
+ "description": "是否将灯光从“手动控制”列表中添加(“true”)或删除(“false”)。🔒",
+ "name": "manual_control"
+ }
+ }
+ },
+ "change_switch_settings": {
+ "name": "更改开关设置",
+ "description": "在开关中更改您想要的任何设置。此处的所有选项与配置流中的选项相同。",
+ "fields": {
+ "entity_id": {
+ "description": "开关的实体ID。📝",
+ "name": "entity_id"
+ },
+ "use_defaults": {
+ "description": "设置未在此服务调用中指定的默认值。选项:“current”(默认值,保留当前值)、“factory”(重置为文档默认值)或“configuration”(恢复到开关配置默认值)。⚙️",
+ "name": "use_defaults"
+ },
+ "include_config_in_attributes": {
+ "description": "在Home Assistant中将所有选项显示为开关的属性时,设置为`true`。📝",
+ "name": "include_config_in_attributes"
+ },
+ "turn_on_lights": {
+ "description": "是否打开当前关闭的灯光。🔆",
+ "name": "turn_on_lights"
+ },
+ "initial_transition": {
+ "description": "灯光从“关闭”到“开启”时的第一个过渡持续时间,以秒为单位。⏲️",
+ "name": "initial_transition"
+ },
+ "sleep_transition": {
+ "description": "切换“睡眠模式”时的过渡持续时间,以秒为单位。😴",
+ "name": "sleep_transition"
+ },
+ "max_brightness": {
+ "description": "最大亮度百分比。💡",
+ "name": "max_brightness"
+ },
+ "max_color_temp": {
+ "description": "最低的色温,以开尔文为单位。❄️",
+ "name": "max_color_temp"
+ },
+ "min_brightness": {
+ "description": "最小亮度百分比。💡",
+ "name": "min_brightness"
+ },
+ "min_color_temp": {
+ "description": "最高的色温,以开尔文为单位。🔥",
+ "name": "min_color_temp"
+ },
+ "only_once": {
+ "description": "仅在打开时调整灯光(`true`)或始终调整灯光(`false`)。🔄",
+ "name": "only_once"
+ },
+ "prefer_rgb_color": {
+ "description": "在可能时是否优先使用RGB颜色调整而不是灯光色温。🌈",
+ "name": "prefer_rgb_color"
+ },
+ "separate_turn_on_commands": {
+ "description": "为某些灯光类型需要使用单独的`light.turn_on`调用来设置颜色和亮度。🔀",
+ "name": "separate_turn_on_commands"
+ },
+ "send_split_delay": {
+ "description": "对于不支持同时设置亮度和颜色的灯光,`separate_turn_on_commands`之间的延迟时间(毫秒)。⏲️",
+ "name": "send_split_delay"
+ },
+ "sleep_brightness": {
+ "description": "睡眠模式中的亮度百分比。😴",
+ "name": "sleep_brightness"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "在睡眠模式中使用“rgb_color”或“color_temp”。🌙",
+ "name": "sleep_rgb_or_color_temp"
+ },
+ "sleep_rgb_color": {
+ "description": "睡眠模式中的RGB颜色(当`sleep_rgb_or_color_temp`为“rgb_color”时使用)。🌈",
+ "name": "sleep_rgb_color"
+ },
+ "sleep_color_temp": {
+ "description": "睡眠模式中的色温(当`sleep_rgb_or_color_temp`为`color_temp`时使用),以开尔文为单位。😴",
+ "name": "sleep_color_temp"
+ },
+ "sunrise_offset": {
+ "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰",
+ "name": "sunrise_offset"
+ },
+ "sunrise_time": {
+ "description": "设置固定的日出时间(HH:MM:SS)。🌅",
+ "name": "sunrise_time"
+ },
+ "sunset_offset": {
+ "description": "以正负偏移秒调整日落时间。⏰",
+ "name": "sunset_offset"
+ },
+ "sunset_time": {
+ "description": "设置固定的日落时间(HH:MM:SS)。🌇",
+ "name": "sunset_time"
+ },
+ "max_sunrise_time": {
+ "description": "设置最晚的虚拟日出时间(HH:MM:SS),允许更早的日出。🌅",
+ "name": "max_sunrise_time"
+ },
+ "min_sunset_time": {
+ "description": "设置最早的虚拟日落时间(HH:MM:SS),允许更晚的日落。🌇",
+ "name": "min_sunset_time"
+ },
+ "take_over_control": {
+ "description": "如果其他来源在灯光处于打开和正在适应状态时调用`light.turn_on`,则禁用自适应照明。请注意,这会每个`interval`调用`homeassistant.update_entity`!🔒",
+ "name": "take_over_control"
+ },
+ "detect_non_ha_changes": {
+ "description": "检测并停止对非`light.turn_on`状态更改的适应。需要启用`take_over_control`。🕵️ 注意:⚠️ 一些灯光可能错误地显示为“开启”状态,这可能导致灯光意外打开。如果遇到此类问题,请禁用此功能。",
+ "name": "detect_non_ha_changes"
+ },
+ "transition": {
+ "description": "灯光变化时的过渡持续时间,以秒为单位。🕑",
+ "name": "transition"
+ },
+ "adapt_delay": {
+ "description": "灯光打开和自适应照明应用更改之间的等待时间(秒)。可能有助于避免闪烁。⏲️",
+ "name": "adapt_delay"
+ },
+ "autoreset_control_seconds": {
+ "description": "在若干秒后自动重置手动控制。设置为0以禁用。⏲️",
+ "name": "autoreset_control_seconds"
+ }
+ }
+ }
+ }
+}
From c0e7eab402f1561e5ddf9cd89cf97a350b7703b7 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Thu, 31 Aug 2023 15:11:18 -0700
Subject: [PATCH 160/549] docs: add lightrabbit as a contributor for
translation (#784)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 1d853b07..48e23abc 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -530,6 +530,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "lightrabbit",
+ "name": "lightrabbit",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1521765?v=4",
+ "profile": "https://github.com/lightrabbit",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 909e36d2..6fbf2beb 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -536,6 +536,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Julien Quiévreux 🌍 |
+  lightrabbit 🌍 |
From a7791a747843c466457b894eaa53822348725fc4 Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Sun, 3 Sep 2023 17:28:35 -0700
Subject: [PATCH 161/549] Add note (#786)
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 6fbf2beb..38f3fc4e 100644
--- a/README.md
+++ b/README.md
@@ -218,6 +218,8 @@ adaptive_lighting:
`adaptive_lighting.change_switch_settings` (new in 1.7.0) Change any of the above configuration options of Adaptive Lighting (such as `sunrise_time` or `prefer_rgb_color`) with a service call directly from your script/automation.
+> ⚠️ **_Note: These settings will **not** be written to your config and will be reset on restart of Home Assistant! You can see the current settings in the `switch.adaptive_lighting_XXX` attributes if `include_config_in_attributes` is enabled._**
+
| Service data attribute | Required | Description |
| --------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `use_defaults` | ❌ | (default: `current` for current settings) Choose from `factory`, `configuration`, or `current` to reset variables not being set with this service call. `current` leaves them as they are, `configuration` resets to initial startup values, `factory` resets to default values listed in the documentation. |
From 5cd060a37c2cdad3c7e266b937dc50e4b1c79711 Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Tue, 12 Sep 2023 22:58:33 +0200
Subject: [PATCH 162/549] Translated using Weblate (Ukrainian) (#788)
Currently translated at 55.5% (85 of 153 strings)
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/uk/
Translation: Adaptive Lighting/Adaptive Lighting
Co-authored-by: Fujitsu Chrome
---
custom_components/adaptive_lighting/translations/uk.json | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/translations/uk.json b/custom_components/adaptive_lighting/translations/uk.json
index 90265f2e..866cee2c 100644
--- a/custom_components/adaptive_lighting/translations/uk.json
+++ b/custom_components/adaptive_lighting/translations/uk.json
@@ -38,7 +38,9 @@
"sunset_time": "sunset_time: Ручний перезапис часу заходу сонця, якщо 'None', тоді використовується час заходу сонця у вашій локації (HH:MM:SS)",
"take_over_control": "take_over_control: Якщо що-небудь, окрім Адаптивного освітлення, викликає 'light.turn_on', коли світло вже увімкнено, чи адаптувати освітлення допоки світло (або перемикач) перемкнеться (off -> on).",
"detect_non_ha_changes": "detect_non_ha_changes: виявляти всі зміни >10% до освітлення (включаючи ті, що зроблені поза HA), вимагає, щоб 'take_over_control' був включений (виклик 'homeassistant.update_entity' кожного оновлення 'interval'!)",
- "transition": "Час переходу, який застосовується до освітлення (секунди)"
+ "transition": "Час переходу, який застосовується до освітлення (секунди)",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Коли спочатку вмикається світло. Якщо `true`, Адаптивне Освітлення адаптується лише якщо `light.turn_on` було викликано без вказування кольору чи яскравості. ❌🌈 Це в тому числі запобігає адаптації, коли активується сцена. Якщо `false`, Адаптивне Освітлення адаптується не залежно від присутності кольору чи яскравості в першочерговому `service_data`. Потребує активації `take_over_control`. 🕵️ ",
+ "transition_until_sleep": "transition_until_sleep: Коли активовано, адаптивне освітлення буде ставитись до налаштування сну як мінімум, переходячи до цих значень після заходу сонця. 🌙"
}
}
},
From 6502dae19fdf3b6710b0f62ab40240b9c253dff7 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Sat, 23 Sep 2023 17:01:55 +0000
Subject: [PATCH 163/549] Translated using Weblate (Dutch)
Currently translated at 73.8% (113 of 153 strings)
Co-authored-by: Arie6414
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/nl.json | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index 81030b6a..d8f54bcf 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -59,7 +59,10 @@
"interval": "Frequentie om de lampen aan te passen, in seconden. 🔄",
"sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴",
"autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen.",
- "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴"
+ "sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴",
+ "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` `color_temp` is) in Kelvin. 😴",
+ "brightness_mode": "Helderheidsmodus om te gebruiken. Mogelijke waarden zijn `default`, `linear` en `tanh` (gebruikt `brightness_mode_time_dark` en `brightness_mode_time_light`). 📈",
+ "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️"
}
}
},
@@ -91,8 +94,18 @@
},
"autoreset_control_seconds": {
"description": "Herstel de handmatige bediening na een aantal seconden. Stel in op 0 om uit te schakelen."
+ },
+ "sleep_brightness": {
+ "description": "Helderheidspercentage van lampen in slaapmodus. 😴"
+ },
+ "sleep_color_temp": {
+ "description": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` `color_temp` is) in Kelvin. 😴"
+ },
+ "max_color_temp": {
+ "description": "Koudste kleurtemperatuur in Kelvin. ❄️"
}
- }
+ },
+ "description": "Wijzig alle gewenste instellingen in de schakelaar. Alle opties hier zijn hetzelfde als in de configuratie."
},
"apply": {
"fields": {
From 9c72952ed0f32f19c2738bb8f57e401eb8bee9b7 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Sat, 23 Sep 2023 10:19:26 -0700
Subject: [PATCH 164/549] docs: add Arie6414 as a contributor for translation
(#793)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 48e23abc..e1a6341c 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -539,6 +539,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "Arie6414",
+ "name": "Arie6414",
+ "avatar_url": "https://avatars.githubusercontent.com/u/129661911?v=4",
+ "profile": "https://github.com/Arie6414",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 38f3fc4e..9239e0ab 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -539,6 +539,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Julien Quiévreux 🌍 |
 lightrabbit 🌍 |
+  Arie6414 🌍 |
From e08fc141cfdddca8eddbfacad65236815099020d Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Wed, 4 Oct 2023 20:10:12 +0200
Subject: [PATCH 165/549] Translated using Weblate (Czech)
Currently translated at 54.9% (84 of 153 strings)
Co-authored-by: Hosted Weblate
Co-authored-by: Petr Vyleta
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/cs/
Translation: Adaptive Lighting/Adaptive Lighting
---
custom_components/adaptive_lighting/translations/cs.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json
index 58f8fead..e909c0d2 100644
--- a/custom_components/adaptive_lighting/translations/cs.json
+++ b/custom_components/adaptive_lighting/translations/cs.json
@@ -45,7 +45,8 @@
"take_over_control": "take_over_control: Je-li volán 'light.turn_on' z jiného zdroje, než Adaptivním osvětlením, když je světlo již rozsvíceno, přestaňte toto světlo ovládat, dokud není vypnuto -> zapnuto (nebo i vypínačem).",
"detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)",
"transition": "transition: doba přechodu při změně osvětlení (sekundy)",
- "adapt_delay": "adapt_delay: prodleva mezi zapnutím světla ( sekundy) a projevem změny v Adaptivní osvětlení. Může předcházet blikání."
+ "adapt_delay": "adapt_delay: prodleva mezi zapnutím světla ( sekundy) a projevem změny v Adaptivní osvětlení. Může předcházet blikání.",
+ "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙"
}
}
},
From 276dd9e5826f053a43d5bf6650de7be0f327d73e Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Wed, 4 Oct 2023 20:10:12 +0200
Subject: [PATCH 166/549] Translated using Weblate (Portuguese)
Currently translated at 49.0% (75 of 153 strings)
Added translation using Weblate (Portuguese)
Co-authored-by: Hosted Weblate
Co-authored-by: Luis Caetano
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pt/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/pt.json | 42 +++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100644 custom_components/adaptive_lighting/translations/pt.json
diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json
new file mode 100644
index 00000000..f3230f6a
--- /dev/null
+++ b/custom_components/adaptive_lighting/translations/pt.json
@@ -0,0 +1,42 @@
+{
+ "title": "Iluminação Adaptativa",
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "sunrise_offset": {
+ "description": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰"
+ },
+ "only_once": {
+ "description": "Adaptar as luzes apenas quando estão ligadas (`true`) ou continuar a adaptá-las (`false`)."
+ },
+ "sunset_offset": {
+ "description": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰"
+ }
+ }
+ },
+ "apply": {
+ "description": "Aplica as definições atuais da Iluminação Adaptativa às luzes.",
+ "fields": {
+ "lights": {
+ "description": "Uma luz (ou lista de luzes) para a qual serão aplicadas as definições.💡"
+ }
+ }
+ }
+ },
+ "config": {
+ "abort": {
+ "already_configured": "Este dispositivo já está configurado"
+ }
+ },
+ "options": {
+ "step": {
+ "init": {
+ "data_description": {
+ "sunrise_offset": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰",
+ "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰"
+ },
+ "title": "Opções da Iluminação Adaptativa"
+ }
+ }
+ }
+}
From b6d891019fb7cb8b0b3525e61dc7970bfa357dbd Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Wed, 4 Oct 2023 20:10:12 +0200
Subject: [PATCH 167/549] Translated using Weblate (Swedish)
Currently translated at 70.5% (108 of 153 strings)
Co-authored-by: fmarcu
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sv/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/sv.json | 159 +++++++++++++-----
1 file changed, 114 insertions(+), 45 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json
index fe5fb653..ee085f13 100644
--- a/custom_components/adaptive_lighting/translations/sv.json
+++ b/custom_components/adaptive_lighting/translations/sv.json
@@ -1,50 +1,119 @@
{
- "title": "Adaptiv Ljussättning",
- "config": {
- "step": {
- "user": {
- "title": "Välj ett namn för Adaptiv Ljussättning",
- "description": "Varje konfiguration kan innehålla flera ljuskällor!",
- "data": {
- "name": "Namn"
- }
- }
- },
- "abort": {
- "already_configured": "Enheten är redan konfiguerad"
+ "title": "Adaptiv Ljussättning",
+ "config": {
+ "step": {
+ "user": {
+ "title": "Välj ett namn för Adaptiv Ljussättning",
+ "description": "Varje konfiguration kan innehålla flera ljuskällor!",
+ "data": {
+ "name": "Namn"
}
+ }
},
- "options": {
- "step": {
- "init": {
- "title": "Adaptiv Ljussättning Inställningar",
- "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.",
- "data": {
- "lights": "lights, ljuskällor",
- "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras",
- "interval": "interval, Tid mellan uppdateringar i sekunder",
- "max_brightness": "max_brightness, i procent %",
- "max_color_temp": "max_color_temp, i Kelvin",
- "min_brightness": "min_brightness, i %",
- "min_color_temp": "min_color_temp, i Kelvin",
- "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'",
- "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt",
- "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.",
- "sleep_brightness": "sleep_brightness, i %",
- "sleep_color_temp": "sleep_color_temp, i Kelvin",
- "sunrise_offset": "sunrise_offset, i +/- sekunder",
- "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)",
- "sunset_offset": "sunset_offset, i +/- sekunder",
- "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)",
- "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen",
- "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)",
- "transition": "transition, i sekunder"
- }
- }
- },
- "error": {
- "option_error": "Ogiltlig inställning",
- "entity_missing": "Ett valt ljus hittades inte"
- }
+ "abort": {
+ "already_configured": "Enheten är redan konfiguerad"
}
+ },
+ "options": {
+ "step": {
+ "init": {
+ "title": "Adaptiv Ljussättning Inställningar",
+ "description": "Alla inställningar för en Adaptiv Ljussättning komponent. Titeln på inställningarna är desamma som i YAML konfigurationen. Inga inställningar visas om enheten redan är konfigurerad i YAML.",
+ "data": {
+ "lights": "lights, ljuskällor",
+ "initial_transition": "initial_transition, när ljuskällorna går från 'av' till 'på' eller när 'sleep_state' ändras",
+ "interval": "interval, Tid mellan uppdateringar i sekunder",
+ "max_brightness": "max_brightness, i procent %",
+ "max_color_temp": "max_color_temp, i Kelvin",
+ "min_brightness": "min_brightness, i %",
+ "min_color_temp": "min_color_temp, i Kelvin",
+ "only_once": "only_once, Adaptivt justera endast ljuskällorna när de sätts från 'av' till 'på'",
+ "prefer_rgb_color": "prefer_rgb_color, Använd 'rgb_color' över 'color_temp' om möjligt",
+ "separate_turn_on_commands": "separate_turn_on_commands, for each attribute (color, brightness, etc.) in 'light.turn_on', required for some lights.",
+ "sleep_brightness": "sleep_brightness, i %",
+ "sleep_color_temp": "sleep_color_temp, i Kelvin",
+ "sunrise_offset": "sunrise_offset, i +/- sekunder",
+ "sunrise_time": "sunrise_time, i 'HH:MM:SS' format (om 'None', används den faktiskta soluppgången för din position)",
+ "sunset_offset": "sunset_offset, i +/- sekunder",
+ "sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)",
+ "take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen",
+ "detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)",
+ "transition": "transition, i sekunder"
+ },
+ "data_description": {
+ "sleep_color_temp": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴",
+ "sleep_transition": "Dröjsmål för övergång när \"sov läge\" slås på/av, i sekunder. 🕑",
+ "autoreset_control_seconds": "Nollställ automatiskt manuell kontroll efter ett antal sekunder. Sätt till 0 för at avaktivera. ⏲️",
+ "sleep_brightness": "Procent ljusstyrka för lampor i sovläge. 😴",
+ "interval": "Frekvens för att anpassa lamporna, i sekunder. 🔄",
+ "sunrise_offset": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰",
+ "transition": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑",
+ "sunset_offset": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰",
+ "send_split_delay": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️"
+ }
+ }
+ },
+ "error": {
+ "option_error": "Ogiltlig inställning",
+ "entity_missing": "Ett valt ljus hittades inte"
+ }
+ },
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "sleep_brightness": {
+ "description": "Procent ljusstyrka för lampor i sovläge. 😴"
+ },
+ "sunrise_offset": {
+ "description": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰"
+ },
+ "sleep_color_temp": {
+ "description": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴"
+ },
+ "entity_id": {
+ "description": "Enhets-ID för strömbrytaren. 📝"
+ },
+ "sleep_transition": {
+ "description": "Dröjsmål för övergång när \"sov läge\" slås på/av, i sekunder. 🕑"
+ },
+ "autoreset_control_seconds": {
+ "description": "Nollställ automatiskt manuell kontroll efter ett antal sekunder. Sätt till 0 för at avaktivera. ⏲️"
+ },
+ "only_once": {
+ "description": "Anpassa lampor endast när de slås på ('true') eller fortsätt anpassa dem ('false'). 🔄"
+ },
+ "max_color_temp": {
+ "description": "Kallaste färgtemperatur i Kelvin. ❄️"
+ },
+ "sunset_offset": {
+ "description": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰"
+ },
+ "send_split_delay": {
+ "description": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️"
+ },
+ "transition": {
+ "description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑"
+ }
+ },
+ "description": "Ändra vilka inställningar du vill ha i strömbrytaren. All dessa inställningar är likadana som i config flow."
+ },
+ "set_manual_control": {
+ "fields": {
+ "lights": {
+ "description": "Enhets-ID för lampor. Om inget anges väljs alla lampor i strömbrytaren. 💡"
+ }
+ }
+ },
+ "apply": {
+ "description": "Tillämpar nuvarande Adaptiv Ljussätting inställningar till lampor.",
+ "fields": {
+ "lights": {
+ "description": "En lampa (eller en lamplista) till vilka inställningarna tillämpas."
+ },
+ "transition": {
+ "description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑"
+ }
+ }
+ }
+ }
}
From fed52d2cc14488377aedfa3b2301e09d36641d0d Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 4 Oct 2023 13:53:32 -0700
Subject: [PATCH 168/549] docs: add luixcaetano as a contributor for
translation (#803)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index e1a6341c..2f8bc65d 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -548,6 +548,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "luixcaetano",
+ "name": "luixcaetano",
+ "avatar_url": "https://avatars.githubusercontent.com/u/4554163?v=4",
+ "profile": "https://github.com/luixcaetano",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 9239e0ab..ed35bbf7 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -540,6 +540,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Julien Quiévreux 🌍 |
 lightrabbit 🌍 |
 Arie6414 🌍 |
+  luixcaetano 🌍 |
From 304ec8d7224e696dc6a42968e477f72cfa7cd3a2 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 4 Oct 2023 13:53:52 -0700
Subject: [PATCH 169/549] docs: add fmarcu as a contributor for translation
(#804)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 2f8bc65d..290e895f 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -557,6 +557,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "fmarcu",
+ "name": "fmarcu",
+ "avatar_url": "https://avatars.githubusercontent.com/u/81946691?v=4",
+ "profile": "https://github.com/fmarcu",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index ed35bbf7..cc8831a0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -541,6 +541,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 lightrabbit 🌍 |
 Arie6414 🌍 |
 luixcaetano 🌍 |
+  fmarcu 🌍 |
From 2f19dc22f8e5de82d5126c5a34fad5cc53fda157 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Tue, 10 Oct 2023 17:45:53 +0000
Subject: [PATCH 170/549] [pre-commit.ci] pre-commit autoupdate (#770)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.4.0 → v4.5.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.4.0...v4.5.0)
- [github.com/astral-sh/ruff-pre-commit: v0.0.284 → v0.0.292](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.284...v0.0.292)
- [github.com/psf/black: 23.7.0 → 23.9.1](https://github.com/psf/black/compare/23.7.0...23.9.1)
* Fix suggestion
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bas Nijholt
---
.pre-commit-config.yaml | 6 +++---
custom_components/adaptive_lighting/color_and_brightness.py | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e7b8b253..e032e91e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.5.0
hooks:
- id: check-added-large-files
- id: trailing-whitespace
@@ -8,11 +8,11 @@ repos:
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.0.284
+ rev: v0.0.292
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/psf/black
- rev: 23.7.0
+ rev: 23.9.1
hooks:
- id: black
diff --git a/custom_components/adaptive_lighting/color_and_brightness.py b/custom_components/adaptive_lighting/color_and_brightness.py
index 2968fcc0..9441e9e6 100644
--- a/custom_components/adaptive_lighting/color_and_brightness.py
+++ b/custom_components/adaptive_lighting/color_and_brightness.py
@@ -186,10 +186,10 @@ class SunEvents:
def closest_event(self, dt: datetime.datetime) -> tuple[str, float]:
"""Get the closest sunset or sunrise event."""
(prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt)
- if prev_event == SUN_EVENT_SUNRISE or next_event == SUN_EVENT_SUNRISE:
+ if SUN_EVENT_SUNRISE in (prev_event, next_event):
ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts
return SUN_EVENT_SUNRISE, ts_event
- if prev_event == SUN_EVENT_SUNSET or next_event == SUN_EVENT_SUNSET:
+ if SUN_EVENT_SUNSET in (prev_event, next_event):
ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts
return SUN_EVENT_SUNSET, ts_event
msg = "No sunrise or sunset event found."
From 29b2cc76cae0e8316013550829ba37884da47090 Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Tue, 10 Oct 2023 20:07:06 +0200
Subject: [PATCH 171/549] Translated using Weblate (Portuguese) (#806)
Currently translated at 56.8% (87 of 153 strings)
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pt/
Translation: Adaptive Lighting/Adaptive Lighting
Co-authored-by: Luis Caetano
Co-authored-by: Bas Nijholt
---
.../adaptive_lighting/translations/pt.json | 32 ++++++++++++++++++-
1 file changed, 31 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/translations/pt.json b/custom_components/adaptive_lighting/translations/pt.json
index f3230f6a..11184a20 100644
--- a/custom_components/adaptive_lighting/translations/pt.json
+++ b/custom_components/adaptive_lighting/translations/pt.json
@@ -11,6 +11,21 @@
},
"sunset_offset": {
"description": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰"
+ },
+ "turn_on_lights": {
+ "description": "Para ligar luzes que estão neste momento desligadas. 🔆"
+ },
+ "entity_id": {
+ "description": "ID Entidade do interruptor. 📝"
+ },
+ "sleep_transition": {
+ "description": "Duração da transição quando o \"modo dormir\" é alternado em segundos. 😴"
+ },
+ "autoreset_control_seconds": {
+ "description": "Reiniciar o controlo manual automaticamente após um número de segundos. Definir 0 para desativar. ⏲️"
+ },
+ "transition": {
+ "description": "Duração da transição quando as luzes mudam, em segundos. 🕑"
}
}
},
@@ -19,6 +34,9 @@
"fields": {
"lights": {
"description": "Uma luz (ou lista de luzes) para a qual serão aplicadas as definições.💡"
+ },
+ "transition": {
+ "description": "Duração da transição quando as luzes mudam, em segundos. 🕑"
}
}
}
@@ -26,6 +44,12 @@
"config": {
"abort": {
"already_configured": "Este dispositivo já está configurado"
+ },
+ "step": {
+ "user": {
+ "description": "Cada instância pode conter múltiplas luzes!",
+ "title": "Escolha um nome para a instância de Iluminação Adaptativa"
+ }
}
},
"options": {
@@ -33,10 +57,16 @@
"init": {
"data_description": {
"sunrise_offset": "Ajustar a hora do nascer do sol com um offset positivo ou negativo em segundos. ⏰",
- "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰"
+ "sunset_offset": "Ajustar a hora do pôr do sol com um offset positivo ou negativo em segundos. ⏰",
+ "sleep_transition": "Duração da transição quando o \"modo dormir\" é alternado em segundos. 😴",
+ "autoreset_control_seconds": "Reiniciar o controlo manual automaticamente após um número de segundos. Definir 0 para desativar. ⏲️",
+ "transition": "Duração da transição quando as luzes mudam, em segundos. 🕑"
},
"title": "Opções da Iluminação Adaptativa"
}
+ },
+ "error": {
+ "option_error": "Opção inválida"
}
}
}
From 1a74f4f4bdbe6f1f4964c816565d03838d19daf5 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Thu, 19 Oct 2023 06:07:02 +0200
Subject: [PATCH 172/549] Translated using Weblate (Czech)
Currently translated at 100.0% (153 of 153 strings)
Co-authored-by: Hosted Weblate
Co-authored-by: Michael Kmoch
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/cs/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/cs.json | 165 +++++++++++++++++-
1 file changed, 164 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/translations/cs.json b/custom_components/adaptive_lighting/translations/cs.json
index e909c0d2..1f533935 100644
--- a/custom_components/adaptive_lighting/translations/cs.json
+++ b/custom_components/adaptive_lighting/translations/cs.json
@@ -46,7 +46,36 @@
"detect_non_ha_changes": "detect_non_ha_changes: detekuje všechny změny >10% provedených pro osvětlení (také mimo HA), vyžaduje povolení atributu 'take_over_control' (každý 'interval' spouští 'homeassistant.update_entity'!)",
"transition": "transition: doba přechodu při změně osvětlení (sekundy)",
"adapt_delay": "adapt_delay: prodleva mezi zapnutím světla ( sekundy) a projevem změny v Adaptivní osvětlení. Může předcházet blikání.",
- "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙"
+ "transition_until_sleep": "transition_until_sleep: Pokud je zapnuto, Adaptive Lighting bude zacházet s nastavením spánku jako s minimem, na tyto hodnoty přejde po západu slunce. 🌙",
+ "multi_light_intercept": "multi_light_intercept: Zachytí a přizpůsobí volání `light.turn_on`, která se zaměřují na více světel. ➗⚠️ To může vést k rozdělení jednoho volání `light.turn_on` na více volání, např. když jsou světla v různých vypínačích. Vyžaduje, aby bylo povoleno `intercept`.",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Při prvním zapnutí světel. Je-li nastaveno na `true`, AL se přizpůsobí pouze tehdy, je-li vyvoláno `light.turn_on` bez zadání barvy nebo jasu. ❌🌈 Tím se např. zabrání přizpůsobení při aktivaci scény. Pokud je `false`, AL se přizpůsobí bez ohledu na přítomnost barvy nebo jasu v počátečních `service_data`. Vyžaduje zapnutí funkce `take_over_control`. 🕵️ ",
+ "skip_redundant_commands": "skip_redundant_commands: Přeskočí odesílání adaptačních příkazů, jejichž cílový stav se již rovná známému stavu světla. Minimalizuje síťový provoz a v některých situacích zlepšuje odezvu adaptace. 📉Zakažte, pokud se fyzické stavy světel dostanou mimo synchronizaci se zaznamenaným stavem HA.",
+ "intercept": "intercept: Zachytit a přizpůsobit volání `light.turn_on` a umožnit tak okamžité přizpůsobení barev a jasu. 🏎️ Zakažte pro světla, která nepodporují `light.turn_on` s barvou a jasem najednou.",
+ "include_config_in_attributes": "include_config_in_attributes: Zobrazit všechny možnosti jako atributy přepínače v Home Assistant, pokud je nastaveno na `true`. 📝"
+ },
+ "data_description": {
+ "sleep_rgb_or_color_temp": "V režimu spánku se použije buď `\"rgb_color\"`, nebo `\"color_temp\"`. 🌙",
+ "sleep_color_temp": "Teplota barev v režimu spánku (používá se, když `sleep_rgb_or_color_temp` je `color_temp`) v Kelvinech. 😴",
+ "sleep_transition": "Doba trvání přechodu do režimu spánku v sekundách. 😴",
+ "autoreset_control_seconds": "Automatické resetování ručního ovládání po určitém počtu sekund. Nastavením na 0 se vypne. ⏲️",
+ "min_sunset_time": "Nastavte nejbližší virtuální čas západu slunce (HH:MM:SS), abyste mohli nastavit pozdější západ slunce. 🌅",
+ "sleep_brightness": "Jas světel v procentech během režimu spánku",
+ "min_sunrise_time": "Nastavte nejbližší virtuální čas východu slunce (HH:MM:SS), abyste mohli nastavit pozdější východ slunce. 🌅",
+ "interval": "Frekvence přizpůsobení světel v sekundách. 🔄",
+ "adapt_delay": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání. ⏲️",
+ "sleep_rgb_color": "RGB barva v režimu spánku (používané když `sleep_rgb_or_color_temp` je \"rgb_color\"). 🌈",
+ "sunrise_offset": "Upravte čas východu slunce s pozitivním nebo negativním posunem v sekundách. ⏰",
+ "transition": "Doba trvání přechodu změny světel v sekundách. 🕑",
+ "brightness_mode": "Výběr režimu jasu. Možné hodnoty jsou `default`, `linear` a `tanh` (používá `brightness_mode_time_dark` a `brightness_mode_time_light`). 📈",
+ "brightness_mode_time_light": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.",
+ "sunset_offset": "Nastavte čas západu slunce s kladným nebo záporným posunem v sekundách. ⏰",
+ "sunset_time": "Nastavit pevný čas (HH:MM:SS) pro západ slunce. 🌅",
+ "max_sunset_time": "Nastavte nejpozdější virtuální čas západu slunce (HH:MM:SS), což umožňuje dřívější západ slunce. 🌅",
+ "sunrise_time": "Nastavit pevný čas (HH:MM:SS) pro východ slunce. 🌅",
+ "initial_transition": "Doba trvání prvního přechodu, kdy se světla změní z `vypnuto` na `zapnuto`, v sekundách. ⏲️",
+ "brightness_mode_time_dark": "(Ignorováno, pokud `brightness_mode='default'`) Doba trvání v sekundách pro zvýšení/snížení jasu po/před východem/západem slunce. 📈📉.",
+ "max_sunrise_time": "Nastavte nejpozdější virtuální čas východu slunce (HH:MM:SS), což umožňuje dřívější východ slunce. 🌅",
+ "send_split_delay": "Zpoždění (ms) mezi příkazy `separate_turn_on_commands` pro světla, která nepodporují současné nastavení jasu a barvy. ⏲️"
}
}
},
@@ -54,5 +83,139 @@
"option_error": "Neplatná možnost",
"entity_missing": "V aplikaci Home Assistant chybí jedna nebo více vybraných entit osvětlení"
}
+ },
+ "services": {
+ "change_switch_settings": {
+ "fields": {
+ "sleep_brightness": {
+ "description": "Jas světel v procentech během režimu spánku"
+ },
+ "detect_non_ha_changes": {
+ "description": "Zjistí a zastaví adaptace při změnách stavu, které nejsou ve stavu `light.turn_on`. Nutno mít zapnutou funkci `take_over_control`. 🕵️ Upozornění: ⚠️ Některá světla mohou falešně indikovat stav 'zapnuto', což může vést k neočekávanému zapnutí světel. Pokud se s takovými problémy setkáte, zakažte tuto funkci."
+ },
+ "sunrise_offset": {
+ "description": "Upravte čas východu slunce s pozitivním nebo negativním posunem v sekundách. ⏰"
+ },
+ "max_sunrise_time": {
+ "description": "Nastavte nejpozdější virtuální čas východu slunce (HH:MM:SS), což umožňuje dřívější východ slunce. 🌅"
+ },
+ "sleep_color_temp": {
+ "description": "Teplota barev v režimu spánku (používá se, když `sleep_rgb_or_color_temp` je `color_temp`) v Kelvinech. 😴"
+ },
+ "min_brightness": {
+ "description": "Minimální hodnota jasu. 💡"
+ },
+ "min_color_temp": {
+ "description": "Nejvyšší teplota barvy v Kelvinech. 🔥"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "V režimu spánku se použije buď `\"rgb_color\"`, nebo `\"color_temp\"`. 🌙"
+ },
+ "turn_on_lights": {
+ "description": "Zda se mají zapnout světla, která jsou aktuálně vypnutá. 🔆"
+ },
+ "initial_transition": {
+ "description": "Doba trvání prvního přechodu, kdy se světla změní z `vypnuto` na `zapnuto`, v sekundách. ⏲️"
+ },
+ "entity_id": {
+ "description": "Entita ID přepínače. 📝"
+ },
+ "sunrise_time": {
+ "description": "Nastavit pevný čas (HH:MM:SS) pro východ slunce. 🌅"
+ },
+ "include_config_in_attributes": {
+ "description": "Zobrazení všech možností jako atributů přepínače v aplikaci Home Assistant, pokud je zaškrtnuto. 📝"
+ },
+ "max_brightness": {
+ "description": "Maximální hodnota jasu. 💡"
+ },
+ "sleep_rgb_color": {
+ "description": "RGB barva v režimu spánku (používané když `sleep_rgb_or_color_temp` je \"rgb_color\"). 🌈"
+ },
+ "take_over_control": {
+ "description": "Zakáže adaptivní osvětlení, pokud jiný zdroj volá `light.turn_on`, zatímco jsou světla zapnutá a přizpůsobují se. Vemte na vědomí, že `homeassistant.update_entity` volá každý `interval`! 🔒"
+ },
+ "sleep_transition": {
+ "description": "Doba trvání přechodu do režimu spánku v sekundách. 😴"
+ },
+ "autoreset_control_seconds": {
+ "description": "Automatické resetování ručního ovládání po určitém počtu sekund. Nastavením na 0 se vypne. ⏲️"
+ },
+ "adapt_delay": {
+ "description": "Doba čekání (v sekundách) mezi zapnutím světla a změnou adaptivního osvětlení. Mohlo by to pomoci zabránit blikání."
+ },
+ "only_once": {
+ "description": "Přizpůsobit světla pouze při zapnutí(`true`) nebo je nechat pokaždé přizpůsobit (`false`). 🔄"
+ },
+ "use_defaults": {
+ "description": "Nastaví výchozí hodnoty, které nebyly zadány v tomto volání služby. Možnosti: \"stávající\" (výchozí, zachovává aktuální hodnoty), \"výchozí\" (obnovuje do výchozích hodnot) nebo \"konfigurace\" (vrací výchozí hodnoty konfigurace přepínače). ⚙️"
+ },
+ "separate_turn_on_commands": {
+ "description": "Použití samostatných volání `light.turn_on` pro barvu a jas, které jsou potřebné pro některé typy světel. 🔀"
+ },
+ "prefer_rgb_color": {
+ "description": "Zda upřednostnit nastavení barev RGB před teplotou barev světla, pokud je to možné. 🌈"
+ },
+ "max_color_temp": {
+ "description": "Nejchladnější teplota barvy v Kelvinech. ❄️"
+ },
+ "sunset_offset": {
+ "description": "Nastavte čas západu slunce s kladným nebo záporným posunem v sekundách. ⏰"
+ },
+ "send_split_delay": {
+ "description": "Zpoždění (ms) mezi příkazy `separate_turn_on_commands` pro světla, která nepodporují současné nastavení jasu a barvy. ⏲️"
+ },
+ "sunset_time": {
+ "description": "Nastavit pevný čas (HH:MM:SS) pro západ slunce. 🌅"
+ },
+ "transition": {
+ "description": "Doba trvání přechodu změny světel v sekundách. 🕑"
+ },
+ "min_sunset_time": {
+ "description": "Nastavte nejbližší virtuální čas západu slunce (HH:MM:SS), abyste mohli nastavit pozdější západ slunce. 🌅"
+ }
+ },
+ "description": "V přepínači změňte libovolné nastavení. Všechny možnosti jsou zde stejné jako v konfiguračním souboru."
+ },
+ "apply": {
+ "fields": {
+ "entity_id": {
+ "description": "`entity_id` přepínače s nastavením, které se má použít. 📝"
+ },
+ "adapt_brightness": {
+ "description": "Přizpůsobení jasu světla. 🌞"
+ },
+ "turn_on_lights": {
+ "description": "Zda se mají zapnout světla, která jsou aktuálně vypnutá. 🔆"
+ },
+ "adapt_color": {
+ "description": "Zda se má přizpůsobit barva na podporovaných světlech. 🌈"
+ },
+ "prefer_rgb_color": {
+ "description": "Zda upřednostnit nastavení barev RGB před teplotou barev světla, pokud je to možné. 🌈"
+ },
+ "lights": {
+ "description": "Světlo (nebo seznam světel), na které se má nastavení použít. 💡"
+ },
+ "transition": {
+ "description": "Doba trvání přechodu změny světel v sekundách. 🕑"
+ }
+ },
+ "description": "Aplikuje současné nastavení Adaptivního osvětlení na světla."
+ },
+ "set_manual_control": {
+ "fields": {
+ "manual_control": {
+ "description": "Zda přidat (\"true\") nebo odebrat (\"false\") světlo ze seznamu \"manual_control\". 🔒"
+ },
+ "entity_id": {
+ "description": "`entity_id` spínače, ve kterém se světlo (ne)označí jako `ručně ovládané`. 📝"
+ },
+ "lights": {
+ "description": "entity_id(s) světel, pokud není zadáno jinak, jsou vybrána všechna světla ve spínači. 💡"
+ }
+ },
+ "description": "Označte, zda je světlo \"ručně ovládané\"."
+ }
}
}
From 8bb2ed1de2f8519c643e5675e27276e4b8f234a3 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Thu, 19 Oct 2023 06:07:02 +0200
Subject: [PATCH 173/549] Translated using Weblate (Dutch)
Currently translated at 76.4% (117 of 153 strings)
Co-authored-by: Fred
Co-authored-by: Hosted Weblate
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/nl.json | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index d8f54bcf..7a9b7896 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -62,7 +62,9 @@
"sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴",
"sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` `color_temp` is) in Kelvin. 😴",
"brightness_mode": "Helderheidsmodus om te gebruiken. Mogelijke waarden zijn `default`, `linear` en `tanh` (gebruikt `brightness_mode_time_dark` en `brightness_mode_time_light`). 📈",
- "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️"
+ "send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️",
+ "transition": "Duur van de overgang, in seconden. 🕑",
+ "initial_transition": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️"
}
}
},
@@ -103,6 +105,12 @@
},
"max_color_temp": {
"description": "Koudste kleurtemperatuur in Kelvin. ❄️"
+ },
+ "initial_transition": {
+ "description": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️"
+ },
+ "take_over_control": {
+ "description": "Schakel Adaptive Lighting uit wanneer een andere bron `light.turn_on` aanroept terwijl de lampen aan staan en worden aangepast. N.b. dit zal `homeassistant.update_entity` elke `interval` uitvoeren."
}
},
"description": "Wijzig alle gewenste instellingen in de schakelaar. Alle opties hier zijn hetzelfde als in de configuratie."
From 3162c0ab32e9f502683ae85148a1ded472a83ee7 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Thu, 19 Oct 2023 06:07:02 +0200
Subject: [PATCH 174/549] Translated using Weblate (Swedish)
Currently translated at 100.0% (153 of 153 strings)
Translated using Weblate (Swedish)
Currently translated at 92.1% (141 of 153 strings)
Co-authored-by: Hosted Weblate
Co-authored-by: fmarcu
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/sv/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/sv.json | 103 +++++++++++++++++-
1 file changed, 99 insertions(+), 4 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/sv.json b/custom_components/adaptive_lighting/translations/sv.json
index ee085f13..df81271e 100644
--- a/custom_components/adaptive_lighting/translations/sv.json
+++ b/custom_components/adaptive_lighting/translations/sv.json
@@ -11,7 +11,7 @@
}
},
"abort": {
- "already_configured": "Enheten är redan konfiguerad"
+ "already_configured": "Den här enheten är redan konfiguerad"
}
},
"options": {
@@ -38,7 +38,13 @@
"sunset_time": "sunset_time, i 'HH:MM:SS' format (om 'None', används den faktiskta solnedgången för din position)",
"take_over_control": "take_over_control, om något utöver 'Adaptiv Ljussättning' komponenten kallar på 'light.turn_on' när en ljuskälla redan är på, stängs den adaptiva justeringen av tills ljuskällan stängs av -> på igen, alternativt switchen för konfigurationen",
"detect_non_ha_changes": "detect_non_ha_changes, Upptäcker alla ändringar större än 5% gjorda på ljuskällorna som inte kommer från HA. Kräver att 'take_over_control' är påslaget.(Kallar på 'homeassistant.update_entity' vid varje 'interval'!)",
- "transition": "transition, i sekunder"
+ "transition": "transition, i sekunder",
+ "multi_light_intercept": "multi_light_intercept: Fånga upp och anpassa \"light.turn_on\"-anrop som riktar sig mot flera lampor. ➗⚠️ Detta kan resultera i att ett enda `light.turn_on`-anrop delas upp i flera anrop, t.ex. när lamporna är kopplade till olika strömbrytare. Kräver att \"intercept\" är aktiverat.",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: När lampor först tänds. Om satt till \"true\", anpassar AL endast om \"light.turn_on\" anropas utan att ange färg eller ljusstyrka. ❌🌈 Detta förhindrar t.ex. anpassning när en scen aktiveras. Om \"false\" anpassas AL oavsett förekomsten av färg eller ljusstyrka i den initiala \"service_data\". \"takeover_control\" måste vara aktiverat. 🕵️ ",
+ "skip_redundant_commands": "skip_redundant_commands: Hoppa över att skicka anpassningskommandon vars måltillstånd redan är lika med lampans kända tillstånd. Minimerar nätverkstrafik och förbättrar anpassningsförmågan i vissa situationer. 📉 Inaktivera om lampans tillstånd blir osynkroniserade med HA:s registrerade tillstånd.",
+ "intercept": "intercept: Fånga upp och anpassa `light.turn_on`-anrop för att möjliggöra omedelbar anpassning av färg och ljusstyrka. 🏎️ Inaktivera för lampor som inte stöder `light.turn_on` med färg och ljusstyrka.",
+ "transition_until_sleep": "transition_until_sleep: När aktiverat kommer Adaptive Lighting att behandla sömninställningarna som ett minimum och övergå till dessa värden efter solnedgången. 🌙",
+ "include_config_in_attributes": "include_config_in_attributes: Visa alla alternativ som attribut på strömbrytaren i Home Assistant när den är inställd på \"true\". 📝"
},
"data_description": {
"sleep_color_temp": "Färgtemperatur i sovläge (används när `sleep_rgb_or_color_temp` är `color_temp`) i Kelvin. 😴",
@@ -49,7 +55,20 @@
"sunrise_offset": "Justera soluppgångstiden med positiv och negativ förskutning is sekunder. ⏰",
"transition": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑",
"sunset_offset": "Justera solnedgångstiden med positiv och negativ förskutning is sekunder. ⏰",
- "send_split_delay": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️"
+ "send_split_delay": "Dröjsmål (ms) mellan `separate_turn_on_commands` för lampor som inte stödjer samtidiga ljussyrke och färg inställningar. ⏲️",
+ "sleep_rgb_or_color_temp": "Använd antingen`\"rgb_color\"` eller `\"color_temp\"` i sovläge. 🌙",
+ "min_sunset_time": "Ställ in den tidigaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör senare solnedgångar. 🌇",
+ "min_sunrise_time": "Ställ in den tidigaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör senare soluppgångar. 🌅",
+ "adapt_delay": "Väntetid (sekunder) mellan lamptändning och Adaptiv Ljussättning tillämpar ändringar. Kan hjälpa till att undvika flimmer. ⏲️",
+ "sleep_rgb_color": "RGB-färg i sovläge (används när \"sleep_rgb_or_color_temp\" är \"rgb_color\"). 🌈",
+ "sunset_time": "Ställ in en fast tid (TT:MM:SS) för solnedgången. 🌇",
+ "max_sunset_time": "Ställ in den senaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör tidigare solnedgångar. 🌇",
+ "sunrise_time": "Ställ in en fast tid (TT:MM:SS) för soluppgången. 🌅",
+ "initial_transition": "Den första övergångens varaktighet när lampan slås från ”av” till ”på” i sekunder. ⏲️",
+ "max_sunrise_time": "Ställ in den senaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör tidigare soluppgångar. 🌅",
+ "brightness_mode": "Ljusstyrkeinställing att använda. Möjliga värden är \"default\", \"linear\" och \"tanh\" (använder \"brightness_mode_time_dark\" och \"brightness_mode_time_light\"). 📈",
+ "brightness_mode_time_light": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉.",
+ "brightness_mode_time_dark": "(Ignoreras om `brightness_mode='default'`) Varaktigheten i sekunder för att öka/minska ljusstyrkan efter/före soluppgång/solnedgång. 📈📉."
}
}
},
@@ -93,6 +112,60 @@
},
"transition": {
"description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑"
+ },
+ "max_sunrise_time": {
+ "description": "Ställ in den senaste virtuella soluppgångstiden (TT: MM: SS), vilket möjliggör tidigare soluppgångar. 🌅"
+ },
+ "min_brightness": {
+ "description": "Minimal ljusstyrka i procent. 💡"
+ },
+ "min_color_temp": {
+ "description": "Varmaste färgtemperaturen i Kelvin. 🔥"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "Använd antingen`\"rgb_color\"` eller `\"color_temp\"` i sovläge. 🌙"
+ },
+ "turn_on_lights": {
+ "description": "Om att tända lampor som är för närvarande släckta. 🔆"
+ },
+ "initial_transition": {
+ "description": "Den första övergångens varaktighet när lampan slås från ”av” till ”på” i sekunder. ⏲️"
+ },
+ "sunrise_time": {
+ "description": "Ställ in en fast tid (TT:MM:SS) för soluppgången. 🌅"
+ },
+ "include_config_in_attributes": {
+ "description": "Visa alla alternativ som attribut på strömbrytaren i Home Assistant när ”true”. 📝"
+ },
+ "max_brightness": {
+ "description": "Maximal ljusstyrka i procent. 💡"
+ },
+ "sleep_rgb_color": {
+ "description": "RGB-färg i sovläge (används när \"sleep_rgb_or_color_temp\" är \"rgb_color\"). 🌈"
+ },
+ "adapt_delay": {
+ "description": "Väntetid (sekunder) mellan lamptändning och Adaptiv Ljussättning tillämpar ändringar. Kan hjälpa till att undvika flimmer. ⏲️"
+ },
+ "separate_turn_on_commands": {
+ "description": "Använd separata `light.turn_on`anrop för färg och ljusstyrka, behövs för vissa lamptyper. 🔀"
+ },
+ "prefer_rgb_color": {
+ "description": "Om att föredra RGB-färgjustering framför ljusfärgtemperatur när det är möjligt. 🌈"
+ },
+ "sunset_time": {
+ "description": "Ställ in en fast tid (TT:MM:SS) för solnedgången. 🌇"
+ },
+ "min_sunset_time": {
+ "description": "Ställ in den tidigaste virtuella solnedgångstiden (TT: MM: SS), vilket möjliggör senare solnedgångar. 🌇"
+ },
+ "detect_non_ha_changes": {
+ "description": "Upptäcker och stoppar anpassningar för tillståndsändringar som inte är \"light.turn_on\". Behöver \"takeover_control\" aktiverat. 🕵️ Varning: ⚠️ Vissa lampor kan felaktigt indikera ett \"på\"-läge, vilket kan resultera i att lamporna tänds oväntat. Inaktivera den här funktionen om du stöter på sådana problem."
+ },
+ "take_over_control": {
+ "description": "Inaktivera Adaptive Ligting om en annan källa anropar 'light.turn_on' medan lamporna är tända och anpassas. Observera att detta anropar `homeassistant.update_entity` varje `intervall`! 🔒"
+ },
+ "use_defaults": {
+ "description": "Ställer in standardvärden som inte anges i detta serviceanrop. Alternativ: \"current\" (standard, behåller nuvarande värden), \"factory\" (återställer till dokumenterade standardinställningar) eller \"configuration\" (återgår till strömbrytarens standardinställningar). ⚙️"
}
},
"description": "Ändra vilka inställningar du vill ha i strömbrytaren. All dessa inställningar är likadana som i config flow."
@@ -101,8 +174,15 @@
"fields": {
"lights": {
"description": "Enhets-ID för lampor. Om inget anges väljs alla lampor i strömbrytaren. 💡"
+ },
+ "manual_control": {
+ "description": "Lägg till (\"true\") eller ta bort (\"false\") ljuset från listan \"manual_control\". 🔒"
+ },
+ "entity_id": {
+ "description": "Strömbrytarens ”entity_id\" i vilken lampan ska (av)markeras som \"manuellt styrd\". 📝"
}
- }
+ },
+ "description": "Swedish: Markera om en lampa är \"styrd manuellt\"."
},
"apply": {
"description": "Tillämpar nuvarande Adaptiv Ljussätting inställningar till lampor.",
@@ -112,6 +192,21 @@
},
"transition": {
"description": "Dröjsmål för övergång när lampor ändras, i sekunder. 🕑"
+ },
+ "entity_id": {
+ "description": "\"entity_id\" för strömbrytaren med inställningarna som ska tillämpas. 📝"
+ },
+ "adapt_brightness": {
+ "description": "Om lampans ljusstyrka ska anpassas. 🌞"
+ },
+ "turn_on_lights": {
+ "description": "Om att tända lampor som är för närvarande släckta. 🔆"
+ },
+ "adapt_color": {
+ "description": "Om färgen på lampor som stödjer ska anpassas. 🌈"
+ },
+ "prefer_rgb_color": {
+ "description": "Om att föredra RGB-färgjustering framför ljusfärgtemperatur när det är möjligt. 🌈"
}
}
}
From dc98e4b9335ce6b0548b3f4cce7f7876658369a7 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Thu, 19 Oct 2023 06:07:02 +0200
Subject: [PATCH 175/549] Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (153 of 153 strings)
Co-authored-by: Z-weapon
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/zh_Hans/
Translation: Adaptive Lighting/Adaptive Lighting
---
custom_components/adaptive_lighting/translations/zh-Hans.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/translations/zh-Hans.json b/custom_components/adaptive_lighting/translations/zh-Hans.json
index e9b206c6..87d236d1 100644
--- a/custom_components/adaptive_lighting/translations/zh-Hans.json
+++ b/custom_components/adaptive_lighting/translations/zh-Hans.json
@@ -220,7 +220,7 @@
"name": "sleep_color_temp"
},
"sunrise_offset": {
- "description": "Adjust sunrise time with a positive or negative offset in seconds. ⏰",
+ "description": "以秒为单位的正负偏移调整日出时间。⏰",
"name": "sunrise_offset"
},
"sunrise_time": {
From 3f54647ba0bfa13354a1b1590c5b08465d9471bb Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 18 Oct 2023 21:57:20 -0700
Subject: [PATCH 176/549] docs: add michaelkmoch as a contributor for
translation (#814)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 290e895f..92715cd8 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -566,6 +566,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "michaelkmoch",
+ "name": "michaelkmoch",
+ "avatar_url": "https://avatars.githubusercontent.com/u/107689026?v=4",
+ "profile": "https://github.com/michaelkmoch",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index cc8831a0..1ccf2433 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -542,6 +542,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Arie6414 🌍 |
 luixcaetano 🌍 |
 fmarcu 🌍 |
+  michaelkmoch 🌍 |
From 47f7a6e68eda74ca583bdb472a2b6d5aa80f75c5 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 18 Oct 2023 21:57:47 -0700
Subject: [PATCH 177/549] docs: add fbloemhof as a contributor for translation
(#815)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 92715cd8..169d2338 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -575,6 +575,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "fbloemhof",
+ "name": "Fred",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8753211?v=4",
+ "profile": "https://github.com/fbloemhof",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 1ccf2433..7a2137ef 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -543,6 +543,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 luixcaetano 🌍 |
 fmarcu 🌍 |
 michaelkmoch 🌍 |
+  Fred 🌍 |
From 4d1108a3cfa4ed014185c2c6634bfe906fed13f8 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 18 Oct 2023 21:58:02 -0700
Subject: [PATCH 178/549] docs: add Z-weapon as a contributor for translation
(#816)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 5 ++++-
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 169d2338..dd503bc1 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -584,6 +584,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "Z-weapon",
+ "name": "Z-weapon",
+ "avatar_url": "https://avatars.githubusercontent.com/u/13939632?v=4",
+ "profile": "https://github.com/Z-weapon",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 7a2137ef..221ca899 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -545,6 +545,9 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 michaelkmoch 🌍 |
 Fred 🌍 |
+
+  Z-weapon 🌍 |
+
From 98a48ec0711784defb3f4953b86612babe563389 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 23 Oct 2023 14:02:43 -0700
Subject: [PATCH 179/549] [pre-commit.ci] pre-commit autoupdate (#821)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.0.292 → v0.1.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.0.292...v0.1.1)
- [github.com/psf/black: 23.9.1 → 23.10.0](https://github.com/psf/black/compare/23.9.1...23.10.0)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e032e91e..835445c3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,11 +8,11 @@ repos:
- id: mixed-line-ending
args: ["--fix=lf"]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.0.292
+ rev: v0.1.1
hooks:
- id: ruff
args: ["--fix"]
- repo: https://github.com/psf/black
- rev: 23.9.1
+ rev: 23.10.0
hooks:
- id: black
From dce2a35147e3f9c0cba7f47ca4d5c1bfd85f20c2 Mon Sep 17 00:00:00 2001
From: Kyle Bjordahl <3489222+kylebjordahl@users.noreply.github.com>
Date: Sun, 19 Nov 2023 17:21:05 -0800
Subject: [PATCH 180/549] Protect for None attributes after HA core change
(#846)
---
custom_components/adaptive_lighting/switch.py | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py
index 7e910525..9aec8ba1 100644
--- a/custom_components/adaptive_lighting/switch.py
+++ b/custom_components/adaptive_lighting/switch.py
@@ -710,6 +710,10 @@ def _attributes_have_changed(
adapt_color: bool,
context: Context,
) -> bool:
+ # 2023-11-19: HA core no longer removes light domain attributes when off
+ # so we must protect for `None` here
+ # see https://github.com/home-assistant/core/pull/101946
+
if adapt_color:
old_attributes, new_attributes = _add_missing_attributes(
old_attributes,
@@ -718,8 +722,8 @@ def _attributes_have_changed(
if (
adapt_brightness
- and ATTR_BRIGHTNESS in old_attributes
- and ATTR_BRIGHTNESS in new_attributes
+ and old_attributes.get(ATTR_BRIGHTNESS)
+ and new_attributes.get(ATTR_BRIGHTNESS)
):
last_brightness = old_attributes[ATTR_BRIGHTNESS]
current_brightness = new_attributes[ATTR_BRIGHTNESS]
@@ -736,8 +740,8 @@ def _attributes_have_changed(
if (
adapt_color
- and ATTR_COLOR_TEMP_KELVIN in old_attributes
- and ATTR_COLOR_TEMP_KELVIN in new_attributes
+ and old_attributes.get(ATTR_COLOR_TEMP_KELVIN)
+ and new_attributes.get(ATTR_COLOR_TEMP_KELVIN)
):
last_color_temp = old_attributes[ATTR_COLOR_TEMP_KELVIN]
current_color_temp = new_attributes[ATTR_COLOR_TEMP_KELVIN]
@@ -754,8 +758,8 @@ def _attributes_have_changed(
if (
adapt_color
- and ATTR_RGB_COLOR in old_attributes
- and ATTR_RGB_COLOR in new_attributes
+ and old_attributes.get(ATTR_RGB_COLOR)
+ and new_attributes.get(ATTR_RGB_COLOR)
):
last_rgb_color = old_attributes[ATTR_RGB_COLOR]
current_rgb_color = new_attributes[ATTR_RGB_COLOR]
From e8bd39f6d4995500624359e709aad14ef93fbfe5 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Sun, 19 Nov 2023 17:22:37 -0800
Subject: [PATCH 181/549] docs: add kylebjordahl as a contributor for code
(#848)
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index dd503bc1..7ab14c4d 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -593,6 +593,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "kylebjordahl",
+ "name": "Kyle Bjordahl",
+ "avatar_url": "https://avatars.githubusercontent.com/u/3489222?v=4",
+ "profile": "https://github.com/kylebjordahl",
+ "contributions": [
+ "code"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 221ca899..93d2703c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -547,6 +547,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Z-weapon 🌍 |
+  Kyle Bjordahl 💻 |
From f3c1bdb0bde34d85446c7af46ddaf9383d99d279 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Sun, 3 Dec 2023 13:02:19 +0100
Subject: [PATCH 182/549] Translated using Weblate (Polish)
Currently translated at 100.0% (153 of 153 strings)
Co-authored-by: Hosted Weblate
Co-authored-by: Olek Bruks
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/pl/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/pl.json | 88 +++++++++----------
1 file changed, 44 insertions(+), 44 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/pl.json b/custom_components/adaptive_lighting/translations/pl.json
index 6ad9be29..0ec6f6c4 100644
--- a/custom_components/adaptive_lighting/translations/pl.json
+++ b/custom_components/adaptive_lighting/translations/pl.json
@@ -17,10 +17,10 @@
"options": {
"step": {
"init": {
- "title": "Adaptacyjne oświetlenie opcje",
- "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowany w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [this web app](https://basnijholt.github.io/adaptive-lighting). Aby zobaczyć więcej szczegółów odwiedź [official documentation](https://github.com/basnijholt/adaptive-lighting#readme).",
+ "title": "Opcje adaptacyjnego oświetlenia",
+ "description": "Konfiguracja komponentu Adaptacyjnego oświetlenia. Nazwy opcji odpowiadają ustawieniom YAML. Żadne opcje nie są wyświetlane, jeśli są zdefiniowane w konfiguracji YAML. Aby zobaczyć interaktywne wykresy demonstrujące działanie parametrów odwiedź [tą aplikację webową](https://basnijholt.github.io/adaptive-lighting). Aby zobaczyć więcej szczegółów odwiedź [oficjalną dokumentację](https://github.com/basnijholt/adaptive-lighting#readme).",
"data": {
- "lights": "lights: Lista entity_ids, które mają być kontrolowane (może być pusta). 🌟",
+ "lights": "lights: Lista `entity_id`, które mają być kontrolowane (może być pusta). 🌟",
"initial_transition": "initial_transition: When lights turn 'off' to 'on'. (sekund)",
"sleep_transition": "sleep_transition: When 'sleep_state' changes. (sekund)",
"interval": "interval: Time between switch updates. (sekund)",
@@ -29,7 +29,7 @@
"min_brightness": "min_brightness: Minimalna jasność (w procentach). 💡",
"min_color_temp": "min_color_temp: Najcieplejsza temperatura barwowa (w Kelwinach). 🔥",
"only_once": "only_once: Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄",
- "prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła.. 🌈",
+ "prefer_rgb_color": "prefer_rgb_color: Czy w miarę możliwości preferować regulację kolorów RGB zamiast regulacji temperatury barwowej światła. 🌈",
"separate_turn_on_commands": "separate_turn_on_commands: Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀",
"sleep_brightness": "sleep_brightness, Brightness setting for Sleep Mode. (%)",
"sleep_color_temp": "sleep_color_temp: Color temperature setting for Sleep Mode. (Kelvin)",
@@ -37,24 +37,24 @@
"sunrise_time": "sunrise_time: Manual override of the sunrise time, if 'None', it uses the actual sunrise time at your location (HH:MM:SS)",
"sunset_offset": "sunset_offset: How long before(-) or after(+) to define the sunset point of the cycle (+/- sekund)",
"sunset_time": "sunset_time: Manual override of the sunset time, if 'None', it uses the actual sunset time at your location (HH:MM:SS)",
- "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒",
- "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów.",
+ "take_over_control": "take_over_control: Wyłącz adaptowanie oświetlenia, kiedy inna usługa wywoła `light.turn_on`, gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒",
+ "detect_non_ha_changes": "detect_non_ha_changes: Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan \"on\", co może powodować nieoczekiwane włączanie się świateł. Wyłącz to ustawienie, jeżeli doświadczasz takich objawów.",
"transition": "Transition time when applying a change to the lights (sekund)",
"transition_until_sleep": "transition_until_sleep: Gdy włączone, Adaptacyjne oświetlenie będzie traktowało ustawienia spania jako minimalne i przejdzie do nich po zachodzie słońca. 🌙",
- "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone `true` to Adaptacyjne oświetlenie włączy adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji gdy aktywowana jest scena. Gdy wyłączone `false`, Adaptacyjne oświetlenie włączy adaptacje niezależnie czy `service_data zawiera kolor lub jasność`. Potrzebuje włączonej opcji `take_over_control`. 🕵️ ",
- "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przysadkach poprawia szybkość działania. 📉Wyłącz jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.",
- "include_config_in_attributes": "include_config_in_attributes: Gdy włączone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝",
- "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`aby błyskawicznie dostosować kolor i jasność . 🏎️ Wyłącz dla świateł które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Gdy włączone (`true`) to adaptacyjne oświetlenie zastosuje adaptacje tylko jeżeli `light.turn_on` jest wywołane bez konkretnego koloru lub jasności. ❌🌈 To ustawienie zapobiega między innymi adaptacji, gdy aktywowana jest scena. Gdy wyłączone (`false`), adaptacyjne oświetlenie zastosuje adaptacje niezależnie czy `service_data` zawiera kolor lub jasność. Potrzebuje włączonej opcji `take_over_control`. 🕵️ ",
+ "skip_redundant_commands": "skip_redundant_commands: Pomiń wysyłanie polecenia adaptacji, jeżeli stan światła jest taki sam jak docelowy stan adaptacji. Minimalizuje to ruch sieciowy oraz w niektórych przypadkach poprawia szybkość działania. 📉 Wyłącz, jeżeli faktyczny stan światła się nie pokrywa z tym który widnieje w Home Assistant.",
+ "include_config_in_attributes": "include_config_in_attributes: Gdy włączone (`true`) pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant. 📝",
+ "intercept": "intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, aby błyskawicznie dostosować kolor i jasność. 🏎️ Wyłącz dla świateł, które nie akceptują wywołania `light.turn_on` zawierającego kolor i jasność.",
"multi_light_intercept": "multi_light_intercept: Przechwyć i zaadaptuj wywołanie `light.turn_on`, które dotyczą wielu świateł. ➗⚠️ Może to powodować rozdzielenie pojedynczego wywołania `light.turn_on` na wiele wywołań, na przykład gdy światła są przypisane do rożnych instancji. Wymaga włączonej opcji `intercept`."
},
"data_description": {
"interval": "Częstotliwość adaptacji świateł w sekundach. 🔄",
- "transition": "Długość tranzycji do nowego stanu w sekundach. 🕑",
- "initial_transition": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`w sekundach. ⏲️",
- "sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"`trybie spania. 🌙",
+ "transition": "Długość przejścia do nowego stanu (w sekundach). 🕑",
+ "initial_transition": "Długość pierwszego przejścia, gdy światło zostanie przełączone z `off` na `on` (w sekundach). ⏲️",
+ "sleep_rgb_or_color_temp": "Użyj `\"rgb_color\"` albo `\"color_temp\"` w trybie spania. 🌙",
"sleep_color_temp": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴",
- "sleep_rgb_color": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈",
- "sleep_transition": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴",
+ "sleep_rgb_color": "Kolor RGB w trybie spania (używane, gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈",
+ "sleep_transition": "Długość przejścia, gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴",
"sunrise_time": "Ustaw stały czas wschodu słońca (HH:MM:SS). 🌅",
"sleep_brightness": "Jasność świateł w trybie spania (w procentach). 😴",
"min_sunrise_time": "Ustaw czas najwcześniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na opóźnienie wschodu słońca. 🌅",
@@ -62,19 +62,19 @@
"sunrise_offset": "Dostosuj czas wschodu słońca - przesunięcie o +/- sekund. ⏰",
"sunset_time": "Ustaw stały czas zachodu słońca (HH:MM:SS). 🌇",
"min_sunset_time": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇",
- "brightness_mode": "Tryb ustawianie jasności. Dostępne opcje `default`, `linear`, i `tanh` (używa `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈",
+ "brightness_mode": "Tryb ustawiania jasności. Dostępne opcje to `default`, `linear` i `tanh` (używa `brightness_mode_time_dark` i `brightness_mode_time_light`). 📈",
"max_sunset_time": "Ustaw czas najpóźniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na przyspieszenie zachodu słońca. 🌇",
"sunset_offset": "Dostosuj czas zachodu słońca - przesunięcie o +/- sekund. ⏰",
- "brightness_mode_time_dark": "(pomijany gdy`brightness_mode='default'`) Czas w sekundach kiedy jasność będzie: zwiększana przed wschodem słońca/zmniejszana po zachodzie słońca. 📈📉",
- "brightness_mode_time_light": "(pomijany gdy`brightness_mode='default'`) Czas w sekundach kiedy jasność będzie: zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉",
- "autoreset_control_seconds": "Czas po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️",
+ "brightness_mode_time_dark": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana przed wschodem słońca/zmniejszana po zachodzie słońca. 📈📉",
+ "brightness_mode_time_light": "(Pomijany, gdy `brightness_mode='default'`). Czas w sekundach, kiedy jasność będzie zwiększana po wschodzie słońca/zmniejszana przed zachodem słońca. 📈📉",
+ "autoreset_control_seconds": "Czas, po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0, aby wyłączyć. ⏲️",
"send_split_delay": "Opóźnienie (w ms) pomiędzy `separate_turn_on_commands` dla świateł, które nie akceptują jednoczesnego ustawiania jasności i koloru. ⏲️",
- "adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Morze pomóc zredukować migotanie. ⏲️"
+ "adapt_delay": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️"
}
}
},
"error": {
- "option_error": "Błędne opcje",
+ "option_error": "Błędna opcja",
"entity_missing": "Jednego lub więcej wybranych świateł nie można znaleźć w Home Assistant"
}
},
@@ -86,16 +86,16 @@
"description": "`entity_id` przełącznika, którego ustawienia mają być zastosowane. 📝"
},
"lights": {
- "description": "Światło(albo lista świateł), do których mają być zastosowane ustawieniam 💡"
+ "description": "Światło (albo lista świateł), do których mają być zastosowane ustawienia. 💡"
},
"transition": {
- "description": "Długość tranzycji do nowego stanu w sekundach. 🕑"
+ "description": "Długość przejścia do nowego stanu (w sekundach). 🕑"
},
"adapt_color": {
- "description": "Czy adaptować kolor światła"
+ "description": "Czy adaptować kolor światła. 🌈"
},
"adapt_brightness": {
- "description": "Czy adaptować jasność swiatła. 🌞"
+ "description": "Czy adaptować jasność światła. 🌞"
},
"prefer_rgb_color": {
"description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła. 🌈"
@@ -106,16 +106,16 @@
}
},
"set_manual_control": {
- "description": "Zaznacza czy światło jest 'recznie sterowane'.",
+ "description": "Zaznacza czy światło jest \"ręcznie sterowane\".",
"fields": {
"entity_id": {
- "description": "`entity_id` encji przełącznika, w której należy odznaczyć flagę `ręczne sterowanie`. 📝"
+ "description": "`entity_id` encji przełącznika, w której należy zaznaczyć/odznaczyć flagę `ręczne sterowanie`. 📝"
},
"lights": {
- "description": "`entity_id` świateł, dla których należy odznaczyć flagę `ręczne sterowanie`.💡Gdy lista będzie pusta wszystkie światła będą brane pod uwagę."
+ "description": "`entity_id` świateł, dla których należy odznaczyć flagę `ręczne sterowanie`. 💡Gdy lista będzie pusta wszystkie światła będą brane pod uwagę."
},
"manual_control": {
- "description": "Dodaj (\"true\") albo usuń (\"false\") światło z listy \"ręczne sterowanie\". 🔒"
+ "description": "Czy dodać (\"true\"), czy usunąć (\"false\") światło z listy \"ręczne sterowanie\". 🔒"
}
}
},
@@ -123,16 +123,16 @@
"description": "Zmienia dowolny parametr w przełączniku. Wszystkie opcje są takie same jak w konfiguracji.",
"fields": {
"entity_id": {
- "description": "ID encji przełacznika. 📝"
+ "description": "ID encji przełącznika. 📝"
},
"use_defaults": {
- "description": "Jak mają się zmienić ustawienia, które nie są wyszczególnione w tym wywołaniu. Opcje: \"current\" (domyślne, pozostawia obecne ustawienia), \"factory\" (przywraca ustawienia z dokumentacji), albo \"configuration\" (przywraca wartości z konfiguracji przelacznika). ⚙️"
+ "description": "Jak mają się zmienić ustawienia, które nie są wyszczególnione w tym wywołaniu. Opcje: \"current\" (domyślne, pozostawia obecne ustawienia), \"factory\" (przywraca ustawienia z dokumentacji), albo \"configuration\" (przywraca wartości z konfiguracji przełącznika). ⚙️"
},
"include_config_in_attributes": {
- "description": "Gdy włączone `true` pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant."
+ "description": "Gdy włączone (`true`) pokaż ustawienia jako atrybuty w encji przełącznika w Home Assistant."
},
"sleep_transition": {
- "description": "Długość tranzycji gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴"
+ "description": "Długość przejścia, gdy nastąpi włączenie/wyłączenie \"trybu spania\" (w sekundach). 😴"
},
"max_brightness": {
"description": "Maksymalna jasność (w procentach). 💡"
@@ -141,37 +141,37 @@
"description": "Czy włączyć światła, które są aktualnie wyłączone? 🔆"
},
"initial_transition": {
- "description": "Długość pierwszej tranzycji gdy światło zostanie włączone z `off` na `on`(w sekundach). ⏲️"
+ "description": "Długość pierwszego przejścia, gdy światło zostanie przełączone z `off` na `on` (w sekundach). ⏲️"
},
"min_sunset_time": {
"description": "Ustaw czas najwcześniejszego wirtualnego zachodu słońca (HH:MM:SS), pozwala na opóźnienie zachodu słońca. 🌇"
},
"take_over_control": {
- "description": "Wyłącz adaptowanie oświetlenia gdy inna usługa wywoła `light.turn_on` gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co`interval`! 🔒"
+ "description": "Wyłącz adaptowanie oświetlenia, kiedy inna usługa wywoła `light.turn_on`, gdy oświetlenie jest już włączone. Zauważ, że to wywołuje `homeassistant.update_entity` co `interval`! 🔒"
},
"transition": {
- "description": "Długość tranzycji do nowego stanu w sekundach. 🕑"
+ "description": "Długość przejścia do nowego stanu (w sekundach). 🕑"
},
"autoreset_control_seconds": {
- "description": "Czas po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️"
+ "description": "Czas, po którym manualna kontrola zostanie wyłączona (w sekundach). Ustaw 0 aby wyłączyć. ⏲️"
},
"adapt_delay": {
- "description": "Czas (w sekundach) pomiędzy włączeniem światła a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Morze pomóc zredukować migotanie. ⏲️"
+ "description": "Czas (w sekundach) pomiędzy włączeniem światła, a rozpoczęciem adaptowania przez Adaptacyjne oświetlenie. Może pomóc zredukować migotanie. ⏲️"
},
"max_color_temp": {
"description": "Najzimniejsza temperatura barwowa (w Kelwinach). ❄️"
},
"min_brightness": {
- "description": "Minimalna jasność (w procentach). 💡"
+ "description": "Minimalna jasność (w procentach). 💡"
},
"min_color_temp": {
"description": "Najcieplejsza temperatura barwowa (w Kelwinach). 🔥"
},
"only_once": {
- "description": "Adaptuj światło tylko podczas włączenia (`true`) lub adaptuj cały czas (`false`). 🔄"
+ "description": "Adaptuj światło tylko podczas włączania (`true`) lub adaptuj cały czas (`false`). 🔄"
},
"prefer_rgb_color": {
- "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła.. 🌈"
+ "description": "Czy w miarę możliwości preferować regulację kolorów RGB zamiast temperatury barwowej światła. 🌈"
},
"separate_turn_on_commands": {
"description": "Używaj oddzielnych wywołań `light.turn_on` dla koloru i jasności, wymagane dla niektórych typów świateł. 🔀"
@@ -183,10 +183,10 @@
"description": "Jasność świateł w trybie spania (w procentach). 😴"
},
"sleep_rgb_or_color_temp": {
- "description": "Użyj `\"rgb_color\"` albo `\"color_temp\"`trybie spania. 🌙"
+ "description": "Użyj `\"rgb_color\"` albo `\"color_temp\"` w trybie spania. 🌙"
},
"sleep_rgb_color": {
- "description": "Kolor RGB w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈"
+ "description": "Kolor RGB w trybie spania (używane, gdy `sleep_rgb_or_color_temp` jest `rgb_color`). 🌈"
},
"sleep_color_temp": {
"description": "Temperatura barwowa w trybie spania (używane gdy `sleep_rgb_or_color_temp` jest `color_temp`) (w Kelwinach). 😴"
@@ -207,7 +207,7 @@
"description": "Ustaw czas najpóźniejszego wirtualnego wschodu słońca (HH:MM:SS), pozwala na przyspieszenie wschodu słońca. 🌅"
},
"detect_non_ha_changes": {
- "description": "Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan 'on', co może powodować nieoczekiwane włączenia światła. Wyłącz to ustawienie jeżeli doświadczasz takich objawów."
+ "description": "Wykrywa i zatrzymuje adaptacje oświetlenia przy zmianach nie pochodzących od `light.turn_on`. Wymaga aktywnego `take_over_control`. 🕵️ Uwaga: ⚠️ Niektóre światła mogą błędnie wskazywać stan \"on\", co może powodować nieoczekiwane włączanie się świateł. Wyłącz to ustawienie, jeżeli doświadczasz takich objawów."
}
}
}
From 6271dda2cfe6eaadcd1bf9ceafec17bed2ff63f1 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Sun, 3 Dec 2023 13:02:19 +0100
Subject: [PATCH 183/549] Translated using Weblate (Italian)
Currently translated at 100.0% (153 of 153 strings)
Co-authored-by: Gabriele Baldassarre
Co-authored-by: Hosted Weblate
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/it/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/it.json | 152 +++++++++++++++++-
1 file changed, 149 insertions(+), 3 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/it.json b/custom_components/adaptive_lighting/translations/it.json
index 3f8c19e8..015f4f6c 100644
--- a/custom_components/adaptive_lighting/translations/it.json
+++ b/custom_components/adaptive_lighting/translations/it.json
@@ -42,11 +42,35 @@
"transition": "Tempo di transizione quando viene applicata una modifica alle luci (secondi)",
"adapt_delay": "Tempo di attesa tra l'accensione della luce, e Illuminazione Adattiva che applica le modifiche allo stato della luce. Potrebbe evitare sfarfallii.",
"transition_until_sleep": "transition_until_sleep: Quando abilitato, Adaptive Lighting tratterà le impostazioni di sleep come valori minimi, facendo la transizione a questi valori dopo il tramonto. 🌙",
- "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quando accendi le luci la prima volta. Se impostato su `true`, AL adatta solo se `light.turn_on è invocato senza specificare il colore o la luminosità. ❌🌈 Questo, per esempio, previene l'adattamento quando si attiva una scena. Se `false`, AL adatta indipendentemente dalla presenza di colore o luminosità nei `service_data` iniziali. Necessita che `take_over_control` sia abilitato. 🕵️ "
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Quando accendi le luci la prima volta. Se impostato su `true`, AL adatta solo se `light.turn_on è invocato senza specificare il colore o la luminosità. ❌🌈 Questo, per esempio, previene l'adattamento quando si attiva una scena. Se `false`, AL adatta indipendentemente dalla presenza di colore o luminosità nei `service_data` iniziali. Necessita che `take_over_control` sia abilitato. 🕵️ ",
+ "multi_light_intercept": "multi_light_intercept: Intercetta e adatta le chiamate a `light.turn_on` destinate a più luci. ➗⚠️ Questo potrebbe causare la divisione della singola chiamata `light.turn_on`in più chiamate, ad esempio quando le luci sono su switch diversi. Richiede che l'opzione `intercept` sia abilitata.",
+ "skip_redundant_commands": "skip_redundant_commands: Salta l'invio di comandi di adattamento rivolti ad entità in cui lo stato desiderato è identico allo stato attuale. Minimizza il traffico sulla rete e migliora la responsività dell'adattamento in alcune situazioni. 📉 Disabilitalo se lo stato reale delle luci va fuori sincrono con quello registrato da HA.",
+ "intercept": "intercept: Intercetta e adatta alle chiamate a`light.turn_on` per abilitare adattamenti istantanei di colore e luminosità. 🏎️ Disabilita per quelle luci che non supportano l'impostazione di luci e colori a seguito dell'evento `light.turn_on`.",
+ "include_config_in_attributes": "include_config_in_attributes: Quando impostato come `true`, mostra tutte le opzioni come attributi dello switch in Home Assistant. 📝"
},
"data_description": {
"sunrise_offset": "Regola il momento dell'alba con un offset positivo o negativo. ⏰",
- "sunset_offset": "Modifica l'orario del tramonto con un offset in secondi positivo o negativo. ⏰"
+ "sunset_offset": "Modifica l'orario del tramonto con un offset in secondi positivo o negativo. ⏰",
+ "sleep_rgb_or_color_temp": "Usa uno tra `\"rgb_color\"` or`\"color_temp\"` in modalità notturna. 🌙",
+ "sleep_color_temp": "Temperatura colore per la modalità notturna (utilizzata quando `sleep_rgb_or_color_temp` vale `color_temp`), espressa in Kelvin. 😴",
+ "sleep_transition": "Durata della transizione al passaggio da/verso la modalità luce notturna, espressa in secondi. 😴",
+ "autoreset_control_seconds": "Rimuovi automaticamente il colore impostato manualmente dopo un certo numero di secondi. Imposta 0 per disabilitare. ⏲️",
+ "min_sunset_time": "Imposta il minimo orario per il tramonto (HH:MM:SS), per eventualmente ritardarlo. 🌅",
+ "sleep_brightness": "Luminosità percentuale delle luci in modalità notturna. 😴",
+ "min_sunrise_time": "Imposta il minimo orario per l'alba (HH:MM:SS), per eventualmente ritardarla. 🌅",
+ "interval": "Frequenza di adattamento delle luci, espressa in secondi. 🔄",
+ "adapt_delay": "Tempo di attesa (in secondi) tra l'accensione della luce e i cambiamenti indotti da Illuminazione Adattativa. Può contribuire a ridurre lo sfarfallio. ⏲️",
+ "sleep_rgb_color": "Colore RGB in modalità notturna (usato quando `sleep_rgb_or_color_temp` è impostato su \"rgb_color\"). 🌈",
+ "transition": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑",
+ "brightness_mode": "Modalità per la luminosità da utilizzare. I valori possibili sono `default`, `linear`, and `tanh` (usa`brightness_mode_time_dark` e `brightness_mode_time_light`). 📈",
+ "brightness_mode_time_light": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉",
+ "sunset_time": "Imposta un orario fisso (HH:MM:SS) per il tramonto. 🌇",
+ "max_sunset_time": "Imposta il massimo orario per il tramonto (HH:MM:SS), in modo da eventualmente anticiparlo. 🌇",
+ "sunrise_time": "Imposta un orario fisso (HH:MM:SS) per l'alba. 🌅",
+ "initial_transition": "Durata della prima transizione quando le luci passano dallo stato `off` a `on`, espressa in secondi. ⏲️",
+ "brightness_mode_time_dark": "La durata, espressa in secondi, della variazione di luminosità durante le albe/tramonti (ignorato se `brightness_mode='default'`). 📈📉",
+ "max_sunrise_time": "Imposta l'orario massimo per l'alba (HH:MM:SS), in modo da eventualmente anticiparla. 🌅",
+ "send_split_delay": "Ritardo (ms) tra i comandi, per le luci che hanno `separate_turn_on_commands` e che non supportano l'impostazione simultanea di luminosità e colore. ⏲️"
}
}
},
@@ -63,8 +87,130 @@
},
"sunrise_offset": {
"description": "Modifica l'orario dell'alba con un offset in secondi positivo o negativo."
+ },
+ "sleep_brightness": {
+ "description": "Luminosità percentuale delle luci in modalità notturna. 😴"
+ },
+ "detect_non_ha_changes": {
+ "description": "Individua e arresta l'adattamento per i cambiamenti di stato diversi da `light.turn_on`. Richiede che `take_over_control` sia abilitato. 🕵️ Avvertenza: ⚠️ Alcune luci potrebbero riportare erroneamente lo stato di 'on', il che potrebbe causarne accensione inaspettata. Disabilita questa opzione se riscontri questa casistica."
+ },
+ "max_sunrise_time": {
+ "description": "Imposta l'orario massimo per l'alba (HH:MM:SS), in modo da eventualmente anticiparla. 🌅"
+ },
+ "sleep_color_temp": {
+ "description": "Temperatura colore per la modalità notturna (utilizzata quando `sleep_rgb_or_color_temp` vale `color_temp`), espressa in Kelvin. 😴"
+ },
+ "min_brightness": {
+ "description": "Minima luminosità, in percentuale.💡"
+ },
+ "min_color_temp": {
+ "description": "Temperatura colore più calda, espressa in Kelvin.🔥"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "Usa uno tra `\"rgb_color\"` or`\"color_temp\"` in modalità notturna. 🌙"
+ },
+ "turn_on_lights": {
+ "description": "Seleziona per accedere le luci, qualora fossero spente. 🔆"
+ },
+ "initial_transition": {
+ "description": "Durata della prima transizione quando le luci passano dallo stato `off` a `on`, espressa in secondi. ⏲️"
+ },
+ "entity_id": {
+ "description": "ID entità dello switch. 📝"
+ },
+ "sunrise_time": {
+ "description": "Imposta un orario fisso (HH:MM:SS) per l'alba. 🌅"
+ },
+ "include_config_in_attributes": {
+ "description": "Quando impostato su `true`, tutte le opzioni saranno visibili come attributi dello switch in Home Assistant. 📝"
+ },
+ "max_brightness": {
+ "description": "Massima luminosità, in percentuale.💡"
+ },
+ "sleep_rgb_color": {
+ "description": "Colore RGB in modalità notturna (usato quando `sleep_rgb_or_color_temp` è impostato su \"rgb_color\"). 🌈"
+ },
+ "take_over_control": {
+ "description": "Disattiva Illuminazione Adattativa se un'altra sorcente chiama `light.turn_on` mentre le luci sono accese e soggette all'adattamento. Tieni conto che questo comporterà una chiamata a `homeassistant.update_entity` ad ogni `interval`! 🔒"
+ },
+ "sleep_transition": {
+ "description": "Durata della transizione al passaggio da/verso la modalità luce notturna, espressa in secondi. 😴"
+ },
+ "autoreset_control_seconds": {
+ "description": "Rimuovi automaticamente il colore impostato manualmente dopo un certo numero di secondi. Imposta 0 per disabilitare. ⏲️"
+ },
+ "adapt_delay": {
+ "description": "Tempo di attesa (in secondi) tra l'accensione della luce e i cambiamenti indotti da Illuminazione Adattativa. Può contribuire a ridurre lo sfarfallio. ⏲️"
+ },
+ "use_defaults": {
+ "description": "Imposta ai valori predefiniti non specificati nella chiamata al servizio. Opzioni possibili: \"current\" (predefinito, mantiene i valori correnti), \"factory\" (reimposta su un valore predefinito documentato) p \"configuration\" (reimposta sui valori predefiniti dello switch). ⚙️"
+ },
+ "separate_turn_on_commands": {
+ "description": "Usa chiamate distinte a `light.turn_on` per il colore e per la luminosità, necessario per alcuni tipi di luci. 🔀"
+ },
+ "prefer_rgb_color": {
+ "description": "Seleziona per preferire gli aggiustamenti di colore mediante RGB piuttosto che tramite temperatura colore, dove possibile. 🌈"
+ },
+ "max_color_temp": {
+ "description": "Temperatura colore più fredda, espressa in Kelvin. ❄️"
+ },
+ "sunset_offset": {
+ "description": "Modifica l'orario del tramonto con un offset positivo o negativo in secondi. ⏰"
+ },
+ "send_split_delay": {
+ "description": "Ritardo (ms) tra i comandi, per le luci che hanno `separate_turn_on_commands` e che non supportano l'impostazione simultanea di luminosità e colore. ⏲️"
+ },
+ "sunset_time": {
+ "description": "Imposta un orario fisso (HH:MM:SS) per il tramonto. 🌇"
+ },
+ "transition": {
+ "description": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑"
+ },
+ "min_sunset_time": {
+ "description": "Imposta il minimo orario per il tramonto (HH:MM:SS), per eventualmente ritardarlo. 🌅"
}
- }
+ },
+ "description": "Cambia tutte le impostazioni che desideri nello switch. Le opzioni sono le stesse presenti nella procedura di configurazione."
+ },
+ "apply": {
+ "fields": {
+ "entity_id": {
+ "description": "L'`entity_id` dello switch a cui si applicano le impostazioni.📝"
+ },
+ "adapt_brightness": {
+ "description": "Seleziona per adattare la luminosità della luce. 🌞"
+ },
+ "turn_on_lights": {
+ "description": "Seleziona per accedere le luci, qualora fossero spente. 🔆"
+ },
+ "adapt_color": {
+ "description": "Seleziona per adattare il colore, per le luci che lo supportano. 🌈"
+ },
+ "prefer_rgb_color": {
+ "description": "Seleziona per preferire gli aggiustamenti di colore mediante RGB piuttosto che tramite temperatura colore, dove possibile. 🌈"
+ },
+ "lights": {
+ "description": "Una luce (o un insieme di luci) a cui applicare le impostazioni. 💡"
+ },
+ "transition": {
+ "description": "Durata della transizione quando le luci cambiano, espressa in secondi. 🕑"
+ }
+ },
+ "description": "Applica le impostazioni correnti di Illuminazione Adattativa alle luci."
+ },
+ "set_manual_control": {
+ "fields": {
+ "manual_control": {
+ "description": "Seleziona per aggiungere (\"true\") o rimuovere (\"false\") la luce dalla lista di quelle controllate manualmente . 🔒"
+ },
+ "entity_id": {
+ "description": "L'`entity_id` dello switch che controlla se la luce deve operare in modalità manualmente controllata.📝"
+ },
+ "lights": {
+ "description": "entity_id delle luci, se non specificato, tutte le luci nello switch sono selezionate. 💡"
+ }
+ },
+ "description": "Evidenzia quando una luce è controllata manualmente."
}
}
}
From 8c6689cdf2a2e896ea1b62a8567d3bbe2743eb02 Mon Sep 17 00:00:00 2001
From: Hosted Weblate
Date: Sun, 3 Dec 2023 13:02:19 +0100
Subject: [PATCH 184/549] Translated using Weblate (Dutch)
Currently translated at 100.0% (153 of 153 strings)
Co-authored-by: Pepijn Baart
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/nl/
Translation: Adaptive Lighting/Adaptive Lighting
---
.../adaptive_lighting/translations/nl.json | 108 ++++++++++++++++--
1 file changed, 96 insertions(+), 12 deletions(-)
diff --git a/custom_components/adaptive_lighting/translations/nl.json b/custom_components/adaptive_lighting/translations/nl.json
index 7a9b7896..16fe3059 100644
--- a/custom_components/adaptive_lighting/translations/nl.json
+++ b/custom_components/adaptive_lighting/translations/nl.json
@@ -18,16 +18,16 @@
"step": {
"init": {
"title": "Adaptieve verlichting instellingen",
- "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item adaptive_lighting hebt gedefinieerd in uw YAML-configuratie.",
+ "description": "Alle instellingen voor een Adaptieve verlichting component. De optienamen komen overeen met de YAML-instellingen. Er worden geen opties weergegeven als u het item `adaptive_lighting` hebt gedefinieerd in uw YAML-configuratie.\nVoor een demonstratie met interactieve grafieken, parameters en effecten, bezoek [deze web applicatie](https://basnijholt.github.io/adaptive-lighting). Voor verdere details, bekijk de [officiële documentatie](https://github.com/basnijholt/adaptive-lighting#readme).",
"data": {
- "lights": "Lichten",
+ "lights": "Lampen: lijst van `light` entiteiten om te bedienen (kan leeg zijn). 🌟",
"initial_transition": "initial_transition: Wanneer lichten van 'uit' naar 'aan' gaan. (seconden)",
"sleep_transition": "sleep_transition: Wanneer 'sleep_state' verandert. (seconden)",
"interval": "interval: Tijd tussen switch-updates. (seconden)",
"max_brightness": "max_brightness: Hoogste helderheid van lichten tijdens een cyclus. (%)",
"max_color_temp": "max_color_temp: Koudste tint van de kleurtemperatuurcyclus. (kelvin)",
"min_brightness": "min_brightness: Laagste helderheid van lichten tijdens een cyclus. (%)",
- "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (kelvin)",
+ "min_color_temp": "min_color_temp, Warmste tint van de kleurtemperatuurcyclus. (Kelvin)",
"only_once": "only_once: pas de verlichting alleen aan wanneer u ze aanzet.",
"prefer_rgb_color": "prefer_rgb_color: Gebruik waar mogelijk 'rgb_color' in plaats van 'color_temp'.",
"separate_turn_on_commands": "separate_turn_on_commands: Scheid de commando's voor elk attribuut (kleur, helderheid, enz.) in 'light.turn_on' (vereist voor sommige lampen).",
@@ -42,12 +42,12 @@
"sunset_offset": "sunset_offset: Hoe lang voor(-) of na(+) zonsondergang uitvoeren (+/- seconden)",
"sunset_time": "sunset_time: handmatige onderdrukking van de zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsopgangstijd op uw locatie gebruikt (UU:MM:SS)",
"min_sunset_time": "min_sunset_time: handmatige onderdrukking van de minimale zonsondergangstijd, indien 'Geen', wordt de werkelijke zonsondergangstijd op uw locatie gebruikt (UU:MM:SS)",
- "take_over_control": "take_over_control: Als iets anders dan Adaptive Lighting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.",
+ "take_over_control": "take_over_control: Als iets anders dan Adaptieve verlichting 'light.turn_on' roept wanneer een lamp al aan is, stop dan met het aanpassen van het licht totdat het (of de schakelaar) uit -> aan gaat.",
"detect_non_ha_changes": "detect_non_ha_changes: detecteert alle >10% wijzigingen aan de lichten (ook buiten HA), vereist dat 'take_over_control' is ingeschakeld (heet 'homeassistant.update_entity' elke 'interval'!)",
"transition": "Overgangstijd bij het aanbrengen van een wijziging op de lichten (seconden)",
"adapt_delay": "adapt_delay: wachttijd tussen het inschakelen van het licht (seconden) en het aanbrengen van wijzigingen in de lichtstatus door Adaptieve verlichting. Kan flikkering voorkomen.",
- "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past AL alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past AL aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ ",
- "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve Verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙",
+ "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: Bij het initieel inschakelen van de lampen. Als dit op `true` is ingesteld, past Av alleen aan als `light.turn_on` wordt aangeroepen zonder een kleur of helderheid te specificeren. ❌🌈 Dit voorkomt bijvoorbeeld aanpassing bij het activeren van een scène. Als het `false` is, past Av aan ongeacht de aanwezigheid van kleur of helderheid in de initiële `service_data`. `take_over_control` moet ingeschakeld zijn. 🕵️ ",
+ "transition_until_sleep": "transition_until_sleep: Wanneer ingeschakeld, zal Adaptieve verlichting de slaapinstellingen behandelen als het minimum, overgaand naar deze waarden na zonsondergang. 🌙",
"skip_redundant_commands": "skip_redundant_commands: Sla het verzenden van aanpassingscommando's over waarvan de doelstatus al gelijk is aan de bekende status van de lamp. Minimaliseert netwerkverkeer en verbetert de responsiviteit van de aanpassing in sommige situaties. 📉Schakel uit als de fysieke lichtstatus niet meer synchroon loopt met de door HA geregistreerde status.",
"intercept": "intercept: Onderschep en pas `light.turn_on` oproepen aan om directe kleur- en helderheidsaanpassing mogelijk te maken. 🏎️ Schakel uit voor lampen die `light.turn_on` niet ondersteunen met kleur en helderheid.",
"include_config_in_attributes": "include_config_in_attributes: Toon alle opties als attributen op de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝",
@@ -60,11 +60,22 @@
"sleep_transition": "Duur van de overgang in seconden, als slaapstand wordt geactiveerd. 😴",
"autoreset_control_seconds": "Herstel de handmatige bediening automatisch na een aantal seconden. Stel in op 0 om uit te schakelen.",
"sleep_brightness": "Helderheidspercentage van lampen in slaapstand. 😴",
- "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` `color_temp` is) in Kelvin. 😴",
+ "sleep_color_temp": "Kleurtemperatuur in slaapmodus (gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan `color_temp`) in Kelvin. 😴",
"brightness_mode": "Helderheidsmodus om te gebruiken. Mogelijke waarden zijn `default`, `linear` en `tanh` (gebruikt `brightness_mode_time_dark` en `brightness_mode_time_light`). 📈",
"send_split_delay": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️",
- "transition": "Duur van de overgang, in seconden. 🕑",
- "initial_transition": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️"
+ "transition": "Duur van de overgang, in seconden, als lampen aanpassen. 🕑",
+ "initial_transition": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️",
+ "sleep_rgb_or_color_temp": "Gebruik één van beide `\"rgb_color\"` of `\"color_temp\"` in slaapstand. 🌙",
+ "min_sunset_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsondergang, maakt latere zonsondergangen mogelijk. 🌇",
+ "min_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsopkomst, maakt latere zonsopkomsten mogelijk. 🌅",
+ "adapt_delay": "Wachttijd in (seconden) tussen het aanzetten van de lamp en het toepassen van Adaptieve verlichting veranderingen. Het kan helpen om knipperen tegen te gaan. ⏲️",
+ "sleep_rgb_color": "RGB kleur in slaapstand (wordt gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan \"rgb_color\"). 🌈",
+ "brightness_mode_time_light": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.",
+ "sunset_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsondergang. 🌇",
+ "max_sunset_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsondergang, maakt eerdere zonsondergangen mogelijk. 🌇",
+ "sunrise_time": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅",
+ "brightness_mode_time_dark": "(Negeer wanneer `brightness_mode='default'`) De duur in seconden van oplopende/aflopende helderheid na/voor zonsopkomst/zonsondergang. 📈📉.",
+ "max_sunrise_time": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅"
}
}
},
@@ -110,7 +121,58 @@
"description": "Duur van de eerste overgang wanneer de lampen van `uit` naar `aan`gaan, in seconden. ⏲️"
},
"take_over_control": {
- "description": "Schakel Adaptive Lighting uit wanneer een andere bron `light.turn_on` aanroept terwijl de lampen aan staan en worden aangepast. N.b. dit zal `homeassistant.update_entity` elke `interval` uitvoeren."
+ "description": "Schakel Adaptieve verlichting uit wanneer een andere bron `light.turn_on` aanroept terwijl de lampen aan staan en worden aangepast. N.b. dit zal `homeassistant.update_entity` elke `interval` uitvoeren."
+ },
+ "detect_non_ha_changes": {
+ "description": "Detecteert en stopt aanpassingen voor niet-`light.turn_on` state veranderingen. `take_over_control` moet actief zijn. 🕵️ Let op:⚠️Sommige lampen kunnen incorrect een 'on' state weergeven, wat resulteert in lampen die onverwacht aan gaan. Schakel deze feature uit wanneer deze fout zich voordoet."
+ },
+ "max_sunrise_time": {
+ "description": "Stel de tijd (HH:MM:SS) in voor de laatste virtuele zonsopkomst, maakt eerdere zonsopkomsten mogelijk. 🌅"
+ },
+ "min_brightness": {
+ "description": "Minimale helderheid in procenten. 💡"
+ },
+ "min_color_temp": {
+ "description": "Meest warme kleurtemperatuur ins Kelvin. 🔥"
+ },
+ "sleep_rgb_or_color_temp": {
+ "description": "Gebruik één van beide `\"rgb_color\"` of `\"color_temp\"` in slaapstand. 🌙"
+ },
+ "turn_on_lights": {
+ "description": "Of de lampen moeten worden aangezet die momenteel uit zijn.🔆"
+ },
+ "sunrise_time": {
+ "description": "Stel een vaste tijd (HH:MM:SS) in voor zonsopkomst. 🌅"
+ },
+ "include_config_in_attributes": {
+ "description": "Toon alle opties als attributen bij de schakelaar in Home Assistant wanneer ingesteld op `true`. 📝"
+ },
+ "max_brightness": {
+ "description": "Maximale helderheid in procenten. 💡"
+ },
+ "sleep_rgb_color": {
+ "description": "RGB kleur in slaapstand (wordt gebruikt wanneer `sleep_rgb_or_color_temp` gelijk is aan \"rgb_color\"). 🌈"
+ },
+ "adapt_delay": {
+ "description": "Wachttijd in (seconden) tussen het aanzetten van de lamp en het toepassen van Adaptieve verlichting veranderingen. Het kan helpen om knipperen tegen te gaan. ⏲️"
+ },
+ "use_defaults": {
+ "description": "Stelt niet gespecificeerde waarden in voor deze service call. Opties: \"current\" (standaard, behoudt huidige waarden), \"factory\" (herstelt de gedocumenteerde standaardwaarden), of \"configuration\" (zet instellingen terug naar de standaardwaarden in de configuratie ). ⚙️"
+ },
+ "separate_turn_on_commands": {
+ "description": "Gebruik aparte `light.turn_on` calls voor kleur en helderheid, dit is nodig voor bepaalde lampen. 🔀"
+ },
+ "prefer_rgb_color": {
+ "description": "Geef de voorkeur aan RGB kleuren boven de kleurtemperatuur van de lamp wanneer mogelijk. 🌈"
+ },
+ "send_split_delay": {
+ "description": "Vertraging (ms) tussen `separate_turn_on_commands` voor lampen die geen gelijktijdige helderheids- en kleurinstelling ondersteunen. ⏲️"
+ },
+ "sunset_time": {
+ "description": "Stel een vaste tijd (HH:MM:SS) in voor zonsondergang. 🌇"
+ },
+ "min_sunset_time": {
+ "description": "Stel de tijd (HH:MM:SS) in voor de meest vroege virtuele zonsondergang, maakt latere zonsondergangen mogelijk. 🌇"
}
},
"description": "Wijzig alle gewenste instellingen in de schakelaar. Alle opties hier zijn hetzelfde als in de configuratie."
@@ -122,16 +184,38 @@
},
"transition": {
"description": "Duur van de overgang in seconden, als lampen aanpassen. 🕑"
+ },
+ "entity_id": {
+ "description": "De `entity_id` van de schakelaar met de toe te passen instellingen. 📝"
+ },
+ "adapt_brightness": {
+ "description": "Of de helderheid van het licht moet worden aangepast. 🌞"
+ },
+ "turn_on_lights": {
+ "description": "Of de lampen moeten worden aangezet die momenteel uit zijn.🔆"
+ },
+ "adapt_color": {
+ "description": "Aanpassen aan de kleur van de omringende verlichting. 🌈"
+ },
+ "prefer_rgb_color": {
+ "description": "Geef de voorkeur aan RGB kleuren boven de kleurtemperatuur van de lamp wanneer mogelijk. 🌈"
}
},
- "description": "Past de huidige Adaptive Lights instellingen toe op de lampen."
+ "description": "Past de huidige Adaptieve verlichting instellingen toe op de lampen."
},
"set_manual_control": {
"fields": {
"lights": {
"description": "entiteit_id(s) van de lamp(en), indien niets wordt gespecificeerd, worden alle lampen in de schakelaar geselecteerd. 💡"
+ },
+ "manual_control": {
+ "description": "Of de lamp moet worden toegevoegd (`\"true\"`) of verwijderd (`\"false\"`) van de `manual_control` lijst. 🔒"
+ },
+ "entity_id": {
+ "description": "De `entity_id` van de schakelaar waarvan het licht moet worden (on)gemarkeerd als `manually controlled`. 📝"
}
- }
+ },
+ "description": "Geef aan of een lamp 'manually controlled' is."
}
}
}
From 8dfef6d65bfe68c79dc395f5a1d616a76dbf4155 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Thu, 7 Dec 2023 23:48:51 -0800
Subject: [PATCH 185/549] docs: add olekbruks as a contributor for translation
(#863)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 7ab14c4d..64752f00 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -602,6 +602,15 @@
"contributions": [
"code"
]
+ },
+ {
+ "login": "olekbruks",
+ "name": "Olek Bruks",
+ "avatar_url": "https://avatars.githubusercontent.com/u/8738016?v=4",
+ "profile": "https://github.com/olekbruks",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 93d2703c..4ce97ad8 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -548,6 +548,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Z-weapon 🌍 |
 Kyle Bjordahl 💻 |
+  Olek Bruks 🌍 |
From 5f2a67edd8f4462f969bb3f71873e8c27528f549 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Thu, 7 Dec 2023 23:51:11 -0800
Subject: [PATCH 186/549] docs: add theclue as a contributor for translation
(#864)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 64752f00..6df562be 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -611,6 +611,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "theclue",
+ "name": "Gabriele Baldassarre",
+ "avatar_url": "https://avatars.githubusercontent.com/u/1724406?v=4",
+ "profile": "https://gabrielebaldassarre.com",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index 4ce97ad8..d4e081c7 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -549,6 +549,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Z-weapon 🌍 |
 Kyle Bjordahl 💻 |
 Olek Bruks 🌍 |
+  Gabriele Baldassarre 🌍 |
From 4d4da04eb7a1fe35b636599e245cf89575d98e84 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Thu, 7 Dec 2023 23:51:46 -0800
Subject: [PATCH 187/549] docs: add pbaart as a contributor for translation
(#865)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 9 +++++++++
README.md | 3 ++-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 6df562be..9d410308 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -620,6 +620,15 @@
"contributions": [
"translation"
]
+ },
+ {
+ "login": "pbaart",
+ "name": "Pepijn Baart",
+ "avatar_url": "https://avatars.githubusercontent.com/u/2856849?v=4",
+ "profile": "https://github.com/pbaart",
+ "contributions": [
+ "translation"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/README.md b/README.md
index d4e081c7..54adc03e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
[](https://github.com/hacs/integration)

-[](#contributors-)
+[](#contributors-)
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
@@ -550,6 +550,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Kyle Bjordahl 💻 |
 Olek Bruks 🌍 |
 Gabriele Baldassarre 🌍 |
+  Pepijn Baart 🌍 |
From bb29bcc6c5e2ae19030bda0fff77f1fad1d1e6ff Mon Sep 17 00:00:00 2001
From: Bas Nijholt
Date: Thu, 7 Dec 2023 23:52:02 -0800
Subject: [PATCH 188/549] Update manifest.json (#862)
---
custom_components/adaptive_lighting/manifest.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/custom_components/adaptive_lighting/manifest.json b/custom_components/adaptive_lighting/manifest.json
index e93d3897..7461e73d 100644
--- a/custom_components/adaptive_lighting/manifest.json
+++ b/custom_components/adaptive_lighting/manifest.json
@@ -8,5 +8,5 @@
"iot_class": "calculated",
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
"requirements": ["ulid-transform"],
- "version": "1.19.1"
+ "version": "1.20.0"
}
From a247a02b3745ceb742f92b597eb7dffa1766e1f2 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Thu, 7 Dec 2023 23:58:14 -0800
Subject: [PATCH 189/549] docs: add kylebjordahl as a contributor for bug, and
code (#866)
* docs: update README.md
* docs: update .all-contributorsrc
---------
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
---
.all-contributorsrc | 3 ++-
README.md | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 9d410308..b38dac23 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -600,7 +600,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/3489222?v=4",
"profile": "https://github.com/kylebjordahl",
"contributions": [
- "code"
+ "code",
+ "bug"
]
},
{
diff --git a/README.md b/README.md
index 54adc03e..544b03b5 100644
--- a/README.md
+++ b/README.md
@@ -547,7 +547,7 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
 Z-weapon 🌍 |
-  Kyle Bjordahl 💻 |
+  Kyle Bjordahl 💻 🐛 |
 Olek Bruks 🌍 |
 Gabriele Baldassarre 🌍 |
 Pepijn Baart 🌍 |
From c3e4b77c7f11ff18ecff9db75fa968df3f8d7582 Mon Sep 17 00:00:00 2001
From: "Weblate (bot)"
Date: Sat, 9 Dec 2023 07:46:12 +0100
Subject: [PATCH 190/549] Translations update from Hosted Weblate (#867)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Translated using Weblate (Russian)
Currently translated at 80.3% (123 of 153 strings)
Co-authored-by: Artem Pastukhov
Co-authored-by: Hosted Weblate
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/ru/
Translation: Adaptive Lighting/Adaptive Lighting
* Added translation using Weblate (Japanese)
Co-authored-by: Hosted Weblate
Co-authored-by: pantan-cymk
* Translated using Weblate (Hungarian)
Currently translated at 48.3% (74 of 153 strings)
Added translation using Weblate (Hungarian)
Co-authored-by: Hosted Weblate
Co-authored-by: LUKÁCS Miklós
Translate-URL: https://hosted.weblate.org/projects/adaptive-lighting/adaptive-lighting/hu/
Translation: Adaptive Lighting/Adaptive Lighting
* Added translation using Weblate (Finnish)
Co-authored-by: Eero Konttaniemi
Co-authored-by: Hosted Weblate
* Translated using Weblate (Slovak)
Currently translated at 56.8% (87 of 153 strings)
Added translation using Weblate (Slovak)
Co-authored-by: Hosted Weblate