mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-12 06:44:04 +02:00
update from reformat_adapt_lights
This commit is contained in:
commit
b36b17ac03
13 changed files with 562 additions and 173 deletions
|
|
@ -400,7 +400,9 @@
|
|||
"avatar_url": "https://avatars.githubusercontent.com/u/2219836?v=4",
|
||||
"profile": "https://github.com/th3w1zard1",
|
||||
"contributions": [
|
||||
"code"
|
||||
"code",
|
||||
"bug",
|
||||
"maintenance"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
68
.github/ISSUE_TEMPLATE/bug-report.md
vendored
68
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
|
@ -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:
|
||||
<!-- This is where you get to tell us what went wrong. When doing so, please make sure to include *all* relevant information.
|
||||
### Required information for bug reports:
|
||||
|
||||
Please try to include:
|
||||
* What you were doing when you experienced the bug.
|
||||
* Any error messages you saw, *where* you saw them, and what you believe may have caused them (if you have any ideas).
|
||||
* When possible, steps to reliably produce the bug.
|
||||
-->
|
||||
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:
|
||||
|
|
|
|||
7
.github/ISSUE_TEMPLATE/doc.md
vendored
7
.github/ISSUE_TEMPLATE/doc.md
vendored
|
|
@ -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
|
||||
|
|
|
|||
7
.github/ISSUE_TEMPLATE/enhancement.md
vendored
7
.github/ISSUE_TEMPLATE/enhancement.md
vendored
|
|
@ -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: ''
|
||||
|
||||
---
|
||||
|
|
|
|||
7
.github/ISSUE_TEMPLATE/feature.md
vendored
7
.github/ISSUE_TEMPLATE/feature.md
vendored
|
|
@ -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: ''
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
5
.github/workflows/pytest.yaml
vendored
5
.github/workflows/pytest.yaml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
4
.github/workflows/update-readme.yml
vendored
4
.github/workflows/update-readme.yml
vendored
|
|
@ -20,11 +20,11 @@ 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: |
|
||||
pip install markdown-code-runner pandas tabulate
|
||||
pip install markdown-code-runner==1.0.0 pandas tabulate
|
||||
|
||||
- name: Link custom_components/adaptive_lighting
|
||||
run: |
|
||||
|
|
|
|||
11
README.md
11
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)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
|
@ -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
|
|||
<td align="center" valign="top" width="14.28%"><a href="http://medium.com/@hudsonbrendon"><img src="https://avatars.githubusercontent.com/u/5201888?v=4?s=100" width="100px;" alt="Hudson Brendon"/><br /><sub><b>Hudson Brendon</b></sub></a><br /><a href="#translation-hudsonbrendon" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gvssr"><img src="https://avatars.githubusercontent.com/u/61377476?v=4?s=100" width="100px;" alt="Gabriel Visser"/><br /><sub><b>Gabriel Visser</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=gvssr" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/glebsterx"><img src="https://avatars.githubusercontent.com/u/8779304?v=4?s=100" width="100px;" alt="Gleb"/><br /><sub><b>Gleb</b></sub></a><br /><a href="#translation-glebsterx" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ghost"><img src="https://avatars.githubusercontent.com/u/10137?v=4?s=100" width="100px;" alt="Deleted user"/><br /><sub><b>Deleted user</b></sub></a><br /><a href="#translation-ghost" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://omg.dje.li/"><img src="https://avatars.githubusercontent.com/u/103232?v=4?s=100" width="100px;" alt="Avi Miller"/><br /><sub><b>Avi Miller</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=Djelibeybi" title="Documentation">📖</a> <a href="https://github.com/basnijholt/adaptive-lighting/commits?author=Djelibeybi" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/denysdovhan"><img src="https://avatars.githubusercontent.com/u/3459374?v=4?s=100" width="100px;" alt="Denys Dovhan"/><br /><sub><b>Denys Dovhan</b></sub></a><br /><a href="#translation-denysdovhan" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://davidstenbeck.com/"><img src="https://avatars.githubusercontent.com/u/3330933?v=4?s=100" width="100px;" alt="David Stenbeck"/><br /><sub><b>David Stenbeck</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=Davst" title="Documentation">📖</a></td>
|
||||
|
|
@ -445,7 +446,7 @@ These graphs were generated using the values calculated by the Adaptive Lighting
|
|||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brebtatv"><img src="https://avatars.githubusercontent.com/u/10747062?v=4?s=100" width="100px;" alt="Tomáš Valigura"/><br /><sub><b>Tomáš Valigura</b></sub></a><br /><a href="#translation-brebtatv" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/th3w1zard1"><img src="https://avatars.githubusercontent.com/u/2219836?v=4?s=100" width="100px;" alt="Benjamin Auquite"/><br /><sub><b>Benjamin Auquite</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=th3w1zard1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/th3w1zard1"><img src="https://avatars.githubusercontent.com/u/2219836?v=4?s=100" width="100px;" alt="Benjamin Auquite"/><br /><sub><b>Benjamin Auquite</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=th3w1zard1" title="Code">💻</a> <a href="https://github.com/basnijholt/adaptive-lighting/issues?q=author%3Ath3w1zard1" title="Bug reports">🐛</a> <a href="#maintenance-th3w1zard1" title="Maintenance">🚧</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/skycarl"><img src="https://avatars.githubusercontent.com/u/43375685?v=4?s=100" width="100px;" alt="Skyler Carlson"/><br /><sub><b>Skyler Carlson</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=skycarl" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/firstof9"><img src="https://avatars.githubusercontent.com/u/1105672?v=4?s=100" width="100px;" alt="Chris"/><br /><sub><b>Chris</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=firstof9" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/raman325"><img src="https://avatars.githubusercontent.com/u/7243222?v=4?s=100" width="100px;" alt="Raman Gupta"/><br /><sub><b>Raman Gupta</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=raman325" title="Code">💻</a></td>
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
"version": "1.10.0"
|
||||
"requirements": ["ulid-transform"],
|
||||
"version": "1.10.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,8 +85,10 @@ 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 ulid_transform
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import (
|
||||
|
|
@ -181,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(
|
||||
|
|
@ -203,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)
|
||||
|
||||
|
|
@ -217,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):
|
||||
|
|
@ -628,6 +671,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 +714,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 +773,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
|
||||
|
||||
|
||||
|
|
@ -946,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(
|
||||
|
|
@ -1064,7 +1138,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
# 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,
|
||||
|
|
@ -1129,14 +1203,23 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
if lights is None:
|
||||
lights = self._lights
|
||||
|
||||
if not force and self._only_once:
|
||||
return
|
||||
|
||||
filtered_lights = [
|
||||
light
|
||||
for light in lights
|
||||
if force or not self.turn_on_off_listener.transition_timers.get(light)
|
||||
]
|
||||
filtered_lights = []
|
||||
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
|
||||
|
|
@ -1610,33 +1693,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."""
|
||||
|
|
@ -1759,7 +1837,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")
|
||||
|
|
@ -1804,6 +1882,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)
|
||||
|
|
@ -2044,3 +2126,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
|
||||
|
|
|
|||
|
|
@ -5,10 +5,16 @@ 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 / MacOS / Windows PowerShell:
|
||||
```bash
|
||||
docker run -v $(pwd):/app basnijholt/adaptive-lighting:latest
|
||||
docker run -v ${PWD}:/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.
|
||||
|
||||
If you prefer to build the image yourself, use the following command:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Tests for Adaptive Lighting switches."""
|
||||
# pylint: disable=protected-access
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
import datetime
|
||||
import logging
|
||||
from random import randint
|
||||
|
|
@ -48,6 +49,8 @@ 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
|
||||
from homeassistant.components.light import SERVICE_TURN_OFF
|
||||
|
|
@ -61,6 +64,7 @@ from homeassistant.const import (
|
|||
CONF_LIGHTS,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
EVENT_STATE_CHANGED,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
|
|
@ -71,6 +75,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
|
||||
|
|
@ -107,6 +112,15 @@ 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)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_time_zone():
|
||||
|
|
@ -210,6 +224,59 @@ async def setup_lights_and_switch(hass, extra_conf=None):
|
|||
return switch, lights_instances
|
||||
|
||||
|
||||
# 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, {})
|
||||
|
|
@ -237,6 +304,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
|
||||
):
|
||||
|
|
@ -429,6 +497,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)
|
||||
|
|
@ -448,6 +517,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)
|
||||
|
|
@ -595,6 +665,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.2}
|
||||
|
|
@ -623,9 +694,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):
|
||||
|
|
@ -639,6 +716,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)
|
||||
|
|
@ -702,6 +780,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."""
|
||||
|
||||
|
|
@ -752,85 +833,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):
|
||||
|
|
@ -840,14 +843,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 = {
|
||||
|
|
@ -875,12 +870,240 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
@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]
|
||||
|
||||
await turn_light(True, brightness=50)
|
||||
_LOGGER.debug("Test: Brightness set to %s", 50)
|
||||
|
||||
# On next update ENTITY_LIGHT should be marked as manually controlled
|
||||
await update(force=False)
|
||||
assert switch.turn_on_off_listener.last_service_data.get(ENTITY_LIGHT) is not None
|
||||
assert switch.turn_on_off_listener.last_state_change.get(ENTITY_LIGHT) is not None
|
||||
assert switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
|
||||
|
||||
@pytest.mark.dependency(
|
||||
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):
|
||||
|
|
@ -949,6 +1172,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, {})
|
||||
|
|
@ -956,6 +1180,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(
|
||||
|
|
@ -992,6 +1217,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)
|
||||
|
||||
|
|
@ -1028,6 +1254,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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue