mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-11 14:24:03 +02:00
Merge branch 'master' into context-cleanup
This commit is contained in:
commit
34f485881d
38 changed files with 2274 additions and 187 deletions
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
github: [basnijholz, RubenKelevra]
|
||||
17
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: 'Bug Report'
|
||||
about: 'Report a bug in adaptive-lighting.'
|
||||
labels: kind/bug, need/triage
|
||||
---
|
||||
|
||||
#### Version information:
|
||||
|
||||
|
||||
#### Description:
|
||||
<!-- This is where you get to tell us what went wrong. When doing so, please make sure to include *all* relevant information.
|
||||
|
||||
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.
|
||||
-->
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Getting Help on adaptive-lighting
|
||||
url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/q-a
|
||||
about: Q&A section of the discussion tab
|
||||
- name: Share your idea
|
||||
url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/ideas
|
||||
about: And discuss it with the community
|
||||
- name: General discussions about this component
|
||||
url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/general
|
||||
about: General discussions about this component
|
||||
- name: Share your setup with adaptive-lighting
|
||||
url: https://github.com/basnijholt/adaptive-lighting/discussions/categories/show-and-tell
|
||||
about: Or see what other people do with this component
|
||||
13
.github/ISSUE_TEMPLATE/doc.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE/doc.md
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
name: 'Documentation Issue'
|
||||
about: 'Report missing, erroneous docs, broken links or propose new docs'
|
||||
labels: kind/docs_issue, need/triage
|
||||
---
|
||||
|
||||
#### Location
|
||||
|
||||
<!-- In the case of missing/erroneous documentation, where is the error? If possible, a link/URL would be great! -->
|
||||
|
||||
#### Description
|
||||
|
||||
<!-- Describe the documentation issue. -->
|
||||
5
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
5
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
name: 'Enhancement'
|
||||
about: 'Suggest an improvement to an existing feature.'
|
||||
labels: kind/enhancement, need/triage
|
||||
---
|
||||
5
.github/ISSUE_TEMPLATE/feature.md
vendored
Normal file
5
.github/ISSUE_TEMPLATE/feature.md
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
name: 'Feature'
|
||||
about: 'Suggest a new feature'
|
||||
labels: kind/feature, need/triage
|
||||
---
|
||||
6
.github/auto-comment.yml
vendored
Normal file
6
.github/auto-comment.yml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Comment to a new issue.
|
||||
# Disabled
|
||||
# issueOpened: ""
|
||||
|
||||
# Disabled
|
||||
# pullRequestOpened: ""
|
||||
64
.github/config.yml
vendored
Normal file
64
.github/config.yml
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Configuration for welcome - https://github.com/behaviorbot/welcome
|
||||
|
||||
# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome
|
||||
# Comment to be posted to on first time issues
|
||||
newIssueWelcomeComment: >
|
||||
Thank you for submitting your first issue to this repository! A maintainer
|
||||
will be here shortly to triage and review.
|
||||
|
||||
In the meantime, please double-check that you have provided all the
|
||||
necessary information to make this process easy! Any information that can
|
||||
help save additional round trips is useful! We currently aim to give
|
||||
initial feedback within **two business days**. If this does not happen, feel
|
||||
free to leave a comment.
|
||||
|
||||
Please keep an eye on how this issue will be labeled, as labels give an
|
||||
overview of priorities, assignments and additional actions requested by the
|
||||
maintainers:
|
||||
|
||||
- "Priority" labels will show how urgent this is for the team.
|
||||
- "Status" labels will show if this is ready to be worked on, blocked, or in progress.
|
||||
- "Need" labels will indicate if additional input or analysis is required.
|
||||
|
||||
Finally, remember to use [the discussion tab](https://github.com/basnijholt/adaptive-lighting/discussions) if you just need general
|
||||
support.
|
||||
|
||||
# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome
|
||||
# Comment to be posted to on PRs from first time contributors in your repository
|
||||
newPRWelcomeComment: >
|
||||
Thank you for submitting this PR!
|
||||
|
||||
A maintainer will be here shortly to review it.
|
||||
|
||||
We are super grateful! Help us by making sure that:
|
||||
|
||||
* The context for this PR is clear, with relevant discussion, decisions
|
||||
and stakeholders linked/mentioned.
|
||||
|
||||
* Your contribution itself is clear (code comments, self-review for the
|
||||
rest) and in its best form.
|
||||
|
||||
Getting other community members to do a review would be great help too on
|
||||
complex PRs. If you are unsure about something, just leave us a comment.
|
||||
|
||||
Next steps:
|
||||
|
||||
* A maintainer will triage and assign priority to this PR, commenting on
|
||||
any missing things and potentially assigning a reviewer for high
|
||||
priority items.
|
||||
|
||||
* The PR gets reviews, discussed and approvals as needed.
|
||||
|
||||
* The PR is merged by maintainers when it has been approved and comments addressed.
|
||||
|
||||
We currently aim to provide initial feedback/triaging within **two business
|
||||
days**. Please keep an eye on any labelling actions, as these will indicate
|
||||
priorities and status of your contribution.
|
||||
|
||||
We are very grateful for your contribution!
|
||||
|
||||
|
||||
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
|
||||
# Comment to be posted to on pull requests merged by a first time user
|
||||
# Currently disabled
|
||||
#firstPRMergeComment: ""
|
||||
59
.github/workflows/ci.yaml
vendored
Normal file
59
.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
name: pytest
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
runs-on: ubuntu-20.04
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10"]
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.0.2
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.0.2
|
||||
with:
|
||||
repository: home-assistant/core
|
||||
path: core
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.1.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r core/requirements.txt
|
||||
pip install -r core/requirements_test.txt
|
||||
pip install -e core/
|
||||
pip install $(python test_dependencies.py)
|
||||
- name: Run pytest
|
||||
timeout-minutes: 60
|
||||
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
|
||||
cd -
|
||||
|
||||
python3 -X dev -m pytest \
|
||||
-qq \
|
||||
--timeout=9 \
|
||||
--durations=10 \
|
||||
--cov="homeassistant" \
|
||||
--cov-report=xml \
|
||||
-o console_output_style=count \
|
||||
-p no:sugar \
|
||||
tests/components/adaptive_lighting
|
||||
15
.github/workflows/hassfest.yaml
vendored
Normal file
15
.github/workflows/hassfest.yaml
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
name: Validate with hassfest
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
validate_hassfest:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v2"
|
||||
- uses: home-assistant/actions/hassfest@master
|
||||
18
.github/workflows/validate.yml
vendored
Normal file
18
.github/workflows/validate.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: Validate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
validate_hacs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v2"
|
||||
- name: HACS validation
|
||||
uses: "hacs/action@main"
|
||||
with:
|
||||
category: "integration"
|
||||
129
.gitignore
vendored
Normal file
129
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
26
.pre-commit-config.yaml
Normal file
26
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
args: ["--fix=lf"]
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.9.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: 22.6.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.37.3
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ["--py39-plus"]
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
177
README.md
177
README.md
|
|
@ -1,10 +1,172 @@
|
|||
# Adaptive Lighting component
|
||||
[](https://github.com/hacs/integration)
|
||||

|
||||
|
||||
Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in HACS and install it!
|
||||
# Adaptive Lighting component for Home Assistant
|
||||
|
||||
See the documentation at https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/
|
||||

|
||||
|
||||
See [this video on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/) to see how to add the integration and set the options.
|
||||
_Try out this code by adding https://github.com/basnijholt/adaptive-lighting to your custom repos in [HACS (Home Assistant Community Store)](https://hacs.xyz/) and install it!_
|
||||
|
||||
|
||||
The `adaptive_lighting` platform changes the settings of your lights throughout the day.
|
||||
It uses the position of the sun to calculate the color temperature and brightness that is most fitting for that time of the day.
|
||||
Scientific research has shown that this helps to maintain your natural circadian rhythm (your biological clock) and might lead to improved sleep, mood, and general well-being.
|
||||
|
||||
In practical terms, this means that after the sun sets, the brightness of your lights will decrease to a certain minimum brightness, while the color temperature will be at its coolest color temperature at noon, after which it will decrease and reach its warmest color at sunset.
|
||||
Around sunrise, the opposite will happen.
|
||||
|
||||
Additionally, the integration provides a way to define and set your lights in "sleep mode".
|
||||
When "sleep mode" is enabled, the lights will be at a minimal brightness and have a very warm color.
|
||||
|
||||
The integration creates 4 switches (in this example the component's name is `"living_room"`):
|
||||
1. `switch.adaptive_lighting_living_room`, which turns the Adaptive Lighting integration on or off. It has several attributes that show the current light settings.
|
||||
2. `switch.adaptive_lighting_sleep_mode_living_room`, which when activated, turns on "sleep mode" (you can set a specific `sleep_brightness` and `sleep_color_temp`).
|
||||
3. `switch.adaptive_lighting_adapt_brightness_living_room`, which sets whether the integration should adapt the brightness of the lights (if supported by the light).
|
||||
4. `switch.adaptive_lighting_adapt_color_living_room`, which sets whether the integration should adapt the color of the lights (if supported by the light).
|
||||
|
||||
## Taking back control
|
||||
|
||||
Although having your lights automatically adapt is great most of the time, there might be times at which you want to set the lights to a different color/brightness and keep it that way.
|
||||
For this purpose, the integration (when `take_over_control` is enabled) automatically detects whether someone (e.g., person toggling the light switch) or something (automation) changes the lights.
|
||||
If this happens *and* the light is already on, the light that was changed gets marked as "manually controlled" and the Adaptive Lighting component will stop adapting that light until it turns off and on again (or if you use the service call `adaptive_lighting.set_manual_control`).
|
||||
This mechanism works by listening to all `light.turn_on` calls that change the color or brightness and by noting that the component did not make the call.
|
||||
Additionally, there is an option to detect all state changes (when `detect_non_ha_changes` is enabled), so also changes to the lights that were not made by a `light.turn_on` call (e.g., through an app or via something outside of Home Assistant.)
|
||||
It does this by comparing a light's state to Adaptive Lighting's previously used settings.
|
||||
Whenever a light gets marked as "manually controlled", an `adaptive_lighting.manual_control` event is fired, such that one can use this information in automations.
|
||||
|
||||
## Configuration
|
||||
|
||||
This integration is both fully configurable through YAML _and_ the frontend. (**Configuration** -> **Integrations** -> **Adaptive Lighting**, **Adaptive Lighting** -> **Options**)
|
||||
Here, the options in the frontend and in YAML have the same names.
|
||||
|
||||
```yaml
|
||||
# Example configuration.yaml entry
|
||||
adaptive_lighting:
|
||||
lights:
|
||||
- light.living_room_lights
|
||||
```
|
||||
|
||||
### Options
|
||||
| option | description | required | default | type |
|
||||
|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------|-----------|---------|
|
||||
| name | The name to use when displaying this switch. | False | default | string |
|
||||
| lights | List of light entities for Adaptive Lighting to control (may be empty). | False | list | [] |
|
||||
| prefer_rgb_color | Whether to use RGB color adjustment instead of native light color temperature. | False | False | boolean |
|
||||
| initial_transition | How long the first transition is when the lights go from `off` to `on`. | False | 1 | time |
|
||||
| sleep_transition | How long the transition is when when "sleep mode" is toggled | False | 1 | time |
|
||||
| transition | How long the transition is when the lights change, in seconds. | False | 45 | integer |
|
||||
| interval | How often to adapt the lights, in seconds. | False | 90 | integer |
|
||||
| min_brightness | The minimum percent of brightness to set the lights to. | False | 1 | integer |
|
||||
| max_brightness | The maximum percent of brightness to set the lights to. | False | 100 | integer |
|
||||
| min_color_temp | The warmest color temperature to set the lights to, in Kelvin. | False | 2000 | integer |
|
||||
| max_color_temp | The coldest color temperature to set the lights to, in Kelvin. | False | 5500 | integer |
|
||||
| sleep_brightness | Brightness of lights while the sleep mode is enabled. | False | 1 | integer |
|
||||
| sleep_color_temp | Color temperature of lights while the sleep mode is enabled. | False | 1000 | integer |
|
||||
| sunrise_time | Override the sunrise time with a fixed time. | False | time | |
|
||||
| sunrise_offset | Change the sunrise time with a positive or negative offset. | False | 0 | time |
|
||||
| sunset_time | Override the sunset time with a fixed time. | False | time | |
|
||||
| sunset_offset | Change the sunset time with a positive or negative offset. | False | 0 | time |
|
||||
| only_once | Whether to keep adapting the lights (false) or to only adapt the lights as soon as they are turned on (true). | False | False | boolean |
|
||||
| take_over_control | If another source calls `light.turn_on` while the lights are on and being adapted, disable Adaptive Lighting. | False | True | boolean |
|
||||
| detect_non_ha_changes | Whether to detect state changes and stop adapting lights, even not from `light.turn_on`. Needs `take_over_control` to be enabled. Note that by enabling this option, it calls 'homeassistant.update_entity' every 'interval'! | False | False | boolean |
|
||||
| separate_turn_on_commands | Whether to use separate `light.turn_on` calls for color and brightness, needed for some types of lights | False | False | boolean |
|
||||
|
||||
Full example:
|
||||
|
||||
```yaml
|
||||
# Example configuration.yaml entry
|
||||
adaptive_lighting:
|
||||
- name: "default"
|
||||
lights: []
|
||||
prefer_rgb_color: false
|
||||
transition: 45
|
||||
initial_transition: 1
|
||||
interval: 90
|
||||
min_brightness: 1
|
||||
max_brightness: 100
|
||||
min_color_temp: 2000
|
||||
max_color_temp: 5500
|
||||
sleep_brightness: 1
|
||||
sleep_color_temp: 1000
|
||||
sunrise_time: "08:00:00" # override the sunrise time
|
||||
sunrise_offset:
|
||||
sunset_time:
|
||||
sunset_offset: 1800 # in seconds or '00:15:00'
|
||||
take_over_control: true
|
||||
detect_non_ha_changes: false
|
||||
only_once: false
|
||||
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
`adaptive_lighting.apply` applies Adaptive Lighting settings to lights on demand.
|
||||
|
||||
| Service data attribute | Optional | Description |
|
||||
|---------------------------|----------|-------------------------------------------------------------------------|
|
||||
| `entity_id` | no | The `entity_id` of the switch with the settings to apply. |
|
||||
| `lights` | no | A light (or list of lights) to apply the settings to. |
|
||||
| `transition` | yes | The number of seconds for the transition. |
|
||||
| `adapt_brightness` | yes | Whether to change the brightness of the light or not. |
|
||||
| `adapt_color` | yes | Whether to adapt the color on supporting lights. |
|
||||
| `prefer_rgb_color` | yes | Whether to prefer RGB color adjustment over of native light color temperature when possible. |
|
||||
| `turn_on_lights` | yes | Whether to turn on lights that are currently off. |
|
||||
|
||||
`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 | Optional | Description |
|
||||
|------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `entity_id` | no | The `entity_id` of the switch in which to (un)mark the light as being "manually controlled". |
|
||||
| `lights` | yes | entity_id(s) of lights, if not specified, all lights in the switch are selected. |
|
||||
| `manual_control` | yes | Whether to add ('true') or remove ('false') the light from the 'manual_control' list, default: true |
|
||||
|
||||
|
||||
## Automation examples
|
||||
|
||||
Reset the `manual_control` status of a light after an hour.
|
||||
```yaml
|
||||
- alias: "Adaptive lighting: reset manual_control after 1 hour"
|
||||
mode: parallel
|
||||
trigger:
|
||||
platform: event
|
||||
event_type: adaptive_lighting.manual_control
|
||||
variables:
|
||||
light: "{{ trigger.event.data.entity_id }}"
|
||||
switch: "{{ trigger.event.data.switch }}"
|
||||
action:
|
||||
- delay: "01:00:00"
|
||||
- condition: template
|
||||
value_template: "{{ light in state_attr(switch, 'manual_control') }}"
|
||||
- service: adaptive_lighting.set_manual_control
|
||||
data:
|
||||
entity_id: "{{ switch }}"
|
||||
lights: "{{ light }}"
|
||||
manual_control: false
|
||||
```
|
||||
|
||||
Toggle multiple Adaptive Lighting switches to "sleep mode" using an `input_boolean.sleep_mode`.
|
||||
|
||||
```yaml
|
||||
- alias: "Adaptive lighting: toggle 'sleep mode'"
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: input_boolean.sleep_mode
|
||||
- platform: homeassistant
|
||||
event: start # in case the states aren't properly restored
|
||||
variables:
|
||||
sleep_mode: "{{ states('input_boolean.sleep_mode') }}"
|
||||
action:
|
||||
service: "switch.turn_{{ sleep_mode }}"
|
||||
entity_id:
|
||||
- switch.adaptive_lighting_sleep_mode_living_room
|
||||
- switch.adaptive_lighting_sleep_mode_bedroom
|
||||
```
|
||||
|
||||
# Other
|
||||
|
||||
See the documentation of the PR at https://deploy-preview-14877--home-assistant-docs.netlify.app/integrations/adaptive_lighting/ and [this video on Reddit](https://www.reddit.com/r/homeassistant/comments/jabhso/ha_has_it_before_apple_has_even_finished_it_i/) to see how to add the integration and set the options.
|
||||
|
||||
This integration was originally based of the great work of @claytonjn https://github.com/claytonjn/hass-circadian_lighting, but has been 100% rewritten and extended with new features.
|
||||
|
||||
# Having problems?
|
||||
Please enable debug logging by putting this in `configuration.yaml`:
|
||||
|
|
@ -14,7 +176,7 @@ logger:
|
|||
logs:
|
||||
custom_components.adaptive_lighting: debug
|
||||
```
|
||||
and after the problem occurs please create an issue with the log.
|
||||
and after the problem occurs please create an issue with the log (`/config/home-assistant.log`).
|
||||
|
||||
|
||||
### Graphs!
|
||||
|
|
@ -28,3 +190,8 @@ These graphs were generated using the values calculated by the Adaptive Lighting
|
|||
|
||||
##### Brightness:
|
||||

|
||||
|
||||
# Maintainers
|
||||
|
||||
- @basnijholt
|
||||
- @RubenKelevra
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
"""Adaptive Lighting integration in Home-Assistant."""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
from typing import Any
|
||||
|
||||
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,
|
||||
|
|
@ -36,7 +35,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]):
|
||||
async def async_setup(hass: HomeAssistant, config: dict[str, Any]):
|
||||
"""Import integration from config."""
|
||||
|
||||
if DOMAIN in config:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
"""Config flow for Adaptive Lighting integration."""
|
||||
import logging
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
"""Constants for the Adaptive Lighting integration."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import VALID_TRANSITION
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
|
||||
ICON = "mdi:theme-light-dark"
|
||||
|
||||
|
|
@ -17,6 +16,7 @@ CONF_DETECT_NON_HA_CHANGES, DEFAULT_DETECT_NON_HA_CHANGES = (
|
|||
False,
|
||||
)
|
||||
CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION = "initial_transition", 1
|
||||
CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION = "sleep_transition", 1
|
||||
CONF_INTERVAL, DEFAULT_INTERVAL = "interval", 90
|
||||
CONF_MAX_BRIGHTNESS, DEFAULT_MAX_BRIGHTNESS = "max_brightness", 100
|
||||
CONF_MAX_COLOR_TEMP, DEFAULT_MAX_COLOR_TEMP = "max_color_temp", 5500
|
||||
|
|
@ -63,6 +63,7 @@ VALIDATION_TUPLES = [
|
|||
(CONF_LIGHTS, DEFAULT_LIGHTS, cv.entity_ids),
|
||||
(CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR, bool),
|
||||
(CONF_INITIAL_TRANSITION, DEFAULT_INITIAL_TRANSITION, VALID_TRANSITION),
|
||||
(CONF_SLEEP_TRANSITION, DEFAULT_SLEEP_TRANSITION, VALID_TRANSITION),
|
||||
(CONF_TRANSITION, DEFAULT_TRANSITION, VALID_TRANSITION),
|
||||
(CONF_INTERVAL, DEFAULT_INTERVAL, cv.positive_int),
|
||||
(CONF_MIN_BRIGHTNESS, DEFAULT_MIN_BRIGHTNESS, int_between(1, 100)),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
{
|
||||
"domain": "adaptive_lighting",
|
||||
"name": "Adaptive Lighting",
|
||||
"documentation": "https://www.home-assistant.io/integrations/adaptive_lighting",
|
||||
"documentation": "https://github.com/basnijholt/adaptive-lighting#readme",
|
||||
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
|
||||
"config_flow": true,
|
||||
"dependencies": [],
|
||||
"codeowners": ["@basnijholt"],
|
||||
"requirements": []
|
||||
"codeowners": ["@basnijholt", "@RubenKelevra"],
|
||||
"version": "1.0.16",
|
||||
"requirements": [],
|
||||
"iot_class": "calculated"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ apply:
|
|||
description: entity_id of the Adaptive Lighting switch.
|
||||
example: switch.adaptive_lighting_default
|
||||
lights:
|
||||
description: entity_id(s) of lights.
|
||||
description: "entity_id(s) of lights, default: lights of the switch"
|
||||
example: light.bedroom_ceiling
|
||||
transition:
|
||||
description: Transition of the lights.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"title": "Adaptive Lighting",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
|
|
@ -21,7 +20,8 @@
|
|||
"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 go 'off' to 'on' or when 'sleep_state' changes",
|
||||
"initial_transition": "initial_transition, when lights go 'off' to 'on'",
|
||||
"sleep_transition": "sleep_transition, when 'sleep_state' changes",
|
||||
"interval": "interval, time between switch updates in seconds",
|
||||
"max_brightness": "max_brightness, in %",
|
||||
"max_color_temp": "max_color_temp, in Kelvin",
|
||||
|
|
|
|||
|
|
@ -12,11 +12,9 @@ from datetime import timedelta
|
|||
import functools
|
||||
import logging
|
||||
import math
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from typing import Any
|
||||
|
||||
import astral
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
|
|
@ -27,19 +25,27 @@ from homeassistant.components.light import (
|
|||
ATTR_HS_COLOR,
|
||||
ATTR_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_SUPPORTED_COLOR_MODES,
|
||||
ATTR_TRANSITION,
|
||||
ATTR_WHITE_VALUE,
|
||||
ATTR_XY_COLOR,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
COLOR_MODE_BRIGHTNESS,
|
||||
COLOR_MODE_COLOR_TEMP,
|
||||
COLOR_MODE_HS,
|
||||
COLOR_MODE_RGB,
|
||||
COLOR_MODE_RGBW,
|
||||
COLOR_MODE_XY,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
SUPPORT_TRANSITION,
|
||||
SUPPORT_WHITE_VALUE,
|
||||
VALID_TRANSITION,
|
||||
is_on,
|
||||
)
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity
|
||||
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_DOMAIN,
|
||||
|
|
@ -82,6 +88,7 @@ from homeassistant.util.color import (
|
|||
color_xy_to_hs,
|
||||
)
|
||||
import homeassistant.util.dt as dt_util
|
||||
import voluptuous as vol
|
||||
|
||||
from .const import (
|
||||
ADAPT_BRIGHTNESS_SWITCH,
|
||||
|
|
@ -103,6 +110,7 @@ from .const import (
|
|||
CONF_SEPARATE_TURN_ON_COMMANDS,
|
||||
CONF_SLEEP_BRIGHTNESS,
|
||||
CONF_SLEEP_COLOR_TEMP,
|
||||
CONF_SLEEP_TRANSITION,
|
||||
CONF_SUNRISE_OFFSET,
|
||||
CONF_SUNRISE_TIME,
|
||||
CONF_SUNSET_OFFSET,
|
||||
|
|
@ -125,7 +133,6 @@ from .const import (
|
|||
|
||||
_SUPPORT_OPTS = {
|
||||
"brightness": SUPPORT_BRIGHTNESS,
|
||||
"white_value": SUPPORT_WHITE_VALUE,
|
||||
"color_temp": SUPPORT_COLOR_TEMP,
|
||||
"color": SUPPORT_COLOR,
|
||||
"transition": SUPPORT_TRANSITION,
|
||||
|
|
@ -154,7 +161,6 @@ COLOR_ATTRS = { # Should ATTR_PROFILE be in here?
|
|||
|
||||
BRIGHTNESS_ATTRS = {
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_WHITE_VALUE,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_BRIGHTNESS_STEP,
|
||||
ATTR_BRIGHTNESS_STEP_PCT,
|
||||
|
|
@ -176,7 +182,9 @@ def _short_hash(string: str, length: int = 4) -> str:
|
|||
return base64.b85encode(str_hash_bytes)[:length]
|
||||
|
||||
|
||||
def create_context(name: str, which: str, index: int) -> Context:
|
||||
def create_context(
|
||||
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
|
||||
# too long (max len == 36) to fit in the database.
|
||||
|
|
@ -185,23 +193,52 @@ def create_context(name: str, which: str, index: int) -> Context:
|
|||
# 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]
|
||||
return Context(id=context_id)
|
||||
parent_id = parent.id if parent else None
|
||||
return Context(id=context_id, parent_id=parent_id)
|
||||
|
||||
|
||||
def is_our_context(context: Optional[Context]) -> bool:
|
||||
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)
|
||||
|
||||
|
||||
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, None)
|
||||
service_datas.append(service_data_brightness)
|
||||
|
||||
if not service_datas: # neither adapt_brightness nor adapt_color
|
||||
return [service_data]
|
||||
return service_datas
|
||||
|
||||
|
||||
async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall):
|
||||
"""Handle the entity service apply."""
|
||||
hass = switch.hass
|
||||
data = service_call.data
|
||||
all_lights = _expand_light_groups(hass, data[CONF_LIGHTS])
|
||||
all_lights = data[CONF_LIGHTS]
|
||||
if not all_lights:
|
||||
all_lights = switch._lights
|
||||
all_lights = _expand_light_groups(hass, all_lights)
|
||||
switch.turn_on_off_listener.lights.update(all_lights)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Called 'adaptive_lighting.apply' service with '%s'",
|
||||
data,
|
||||
)
|
||||
for light in all_lights:
|
||||
if data[CONF_TURN_ON_LIGHTS] or is_on(hass, light):
|
||||
await switch._adapt_light( # pylint: disable=protected-access
|
||||
|
|
@ -211,6 +248,7 @@ async def handle_apply(switch: AdaptiveSwitch, service_call: ServiceCall):
|
|||
data[ATTR_ADAPT_COLOR],
|
||||
data[CONF_PREFER_RGB_COLOR],
|
||||
force=True,
|
||||
context=switch.create_context("service", parent=service_call.context),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -228,25 +266,36 @@ async def handle_set_manual_control(switch: AdaptiveSwitch, service_call: Servic
|
|||
if service_call.data[CONF_MANUAL_CONTROL]:
|
||||
for light in all_lights:
|
||||
switch.turn_on_off_listener.manual_control[light] = True
|
||||
_fire_manual_control_event(switch.hass, light, service_call.context)
|
||||
_fire_manual_control_event(switch, light, service_call.context)
|
||||
else:
|
||||
switch.turn_on_off_listener.reset(*all_lights)
|
||||
# pylint: disable=protected-access
|
||||
await switch._adapt_lights(
|
||||
all_lights,
|
||||
transition=switch._initial_transition,
|
||||
force=True,
|
||||
context=switch.create_context("service"),
|
||||
)
|
||||
if switch.is_on:
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
all_lights,
|
||||
transition=switch._initial_transition,
|
||||
force=True,
|
||||
context=switch.create_context("service", parent=service_call.context),
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _fire_manual_control_event(
|
||||
hass: HomeAssistant, light: str, context: Context, is_async=True
|
||||
switch: AdaptiveSwitch, light: str, context: Context, is_async=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
|
||||
fire(f"{DOMAIN}.manual_control", {ATTR_ENTITY_ID: light}, context=context)
|
||||
_LOGGER.debug(
|
||||
"'adaptive_lighting.manual_control' event fired for %s for light %s",
|
||||
switch.entity_id,
|
||||
light,
|
||||
)
|
||||
fire(
|
||||
f"{DOMAIN}.manual_control",
|
||||
{ATTR_ENTITY_ID: light, SWITCH_DOMAIN: switch.entity_id},
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
|
@ -287,7 +336,9 @@ async def async_setup_entry(
|
|||
platform.async_register_entity_service(
|
||||
SERVICE_APPLY,
|
||||
{
|
||||
vol.Required(CONF_LIGHTS): cv.entity_ids,
|
||||
vol.Optional(
|
||||
CONF_LIGHTS, default=[]
|
||||
): cv.entity_ids, # pylint: disable=protected-access
|
||||
vol.Optional(
|
||||
CONF_TRANSITION,
|
||||
default=switch._initial_transition, # pylint: disable=protected-access
|
||||
|
|
@ -324,7 +375,7 @@ def validate(config_entry: ConfigEntry):
|
|||
return data
|
||||
|
||||
|
||||
def match_switch_state_event(event: Event, from_or_to_state: List[str]):
|
||||
def match_switch_state_event(event: Event, from_or_to_state: list[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
|
||||
|
|
@ -336,7 +387,7 @@ def match_switch_state_event(event: Event, from_or_to_state: List[str]):
|
|||
return match
|
||||
|
||||
|
||||
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()
|
||||
turn_on_off_listener = hass.data[DOMAIN][ATTR_TURN_ON_OFF_LISTENER]
|
||||
for light in lights:
|
||||
|
|
@ -357,11 +408,34 @@ 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]
|
||||
return {key for key, value in _SUPPORT_OPTS.items() if supported_features & value}
|
||||
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_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(
|
||||
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).
|
||||
|
||||
|
|
@ -372,17 +446,17 @@ 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)]
|
||||
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
|
||||
delta_r, delta_g, delta_b = ((col1 - col2) for col1, col2 in zip(rgb1, rgb2))
|
||||
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)
|
||||
|
||||
|
||||
def _attributes_have_changed(
|
||||
light: str,
|
||||
old_attributes: Dict[str, Any],
|
||||
new_attributes: Dict[str, Any],
|
||||
old_attributes: dict[str, Any],
|
||||
new_attributes: dict[str, Any],
|
||||
adapt_brightness: bool,
|
||||
adapt_color: bool,
|
||||
context: Context,
|
||||
|
|
@ -405,24 +479,6 @@ def _attributes_have_changed(
|
|||
)
|
||||
return True
|
||||
|
||||
if (
|
||||
adapt_brightness
|
||||
and ATTR_WHITE_VALUE in old_attributes
|
||||
and ATTR_WHITE_VALUE in new_attributes
|
||||
):
|
||||
last_white_value = old_attributes[ATTR_WHITE_VALUE]
|
||||
current_white_value = new_attributes[ATTR_WHITE_VALUE]
|
||||
if abs(current_white_value - last_white_value) > BRIGHTNESS_CHANGE:
|
||||
_LOGGER.debug(
|
||||
"White Value of '%s' significantly changed from %s to %s with"
|
||||
" context.id='%s'",
|
||||
light,
|
||||
last_white_value,
|
||||
current_white_value,
|
||||
context.id,
|
||||
)
|
||||
return True
|
||||
|
||||
if (
|
||||
adapt_color
|
||||
and ATTR_COLOR_TEMP in old_attributes
|
||||
|
|
@ -501,18 +557,24 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES]
|
||||
self._initial_transition = data[CONF_INITIAL_TRANSITION]
|
||||
self._sleep_transition = data[CONF_SLEEP_TRANSITION]
|
||||
self._interval = data[CONF_INTERVAL]
|
||||
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 = min(
|
||||
data[CONF_TRANSITION], self._interval.total_seconds() // 2
|
||||
)
|
||||
self._transition = data[CONF_TRANSITION]
|
||||
_loc = get_astral_location(self.hass)
|
||||
if isinstance(_loc, tuple):
|
||||
# Astral v2.2
|
||||
location, _ = _loc
|
||||
else:
|
||||
# Astral v1
|
||||
location = _loc
|
||||
|
||||
self._sun_light_settings = SunLightSettings(
|
||||
name=self._name,
|
||||
astral_location=get_astral_location(self.hass),
|
||||
astral_location=location,
|
||||
max_brightness=data[CONF_MAX_BRIGHTNESS],
|
||||
max_color_temp=data[CONF_MAX_COLOR_TEMP],
|
||||
min_brightness=data[CONF_MIN_BRIGHTNESS],
|
||||
|
|
@ -524,6 +586,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
sunset_offset=data[CONF_SUNSET_OFFSET],
|
||||
sunset_time=data[CONF_SUNSET_TIME],
|
||||
time_zone=self.hass.config.time_zone,
|
||||
transition=data[CONF_TRANSITION],
|
||||
)
|
||||
|
||||
# Set other attributes
|
||||
|
|
@ -531,16 +594,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
self._state = None
|
||||
|
||||
# Tracks 'off' → 'on' state changes
|
||||
self._on_to_off_event: Dict[str, Event] = {}
|
||||
self._on_to_off_event: dict[str, Event] = {}
|
||||
# Tracks 'on' → 'off' state changes
|
||||
self._off_to_on_event: Dict[str, Event] = {}
|
||||
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] = {}
|
||||
self._locks: dict[str, asyncio.Lock] = {}
|
||||
# To count the number of `Context` instances
|
||||
self._context_cnt: int = 0
|
||||
|
||||
# Set in self._update_attrs_and_maybe_adapt_lights
|
||||
self._settings: Dict[str, Any] = {}
|
||||
self._settings: dict[str, Any] = {}
|
||||
|
||||
# Set and unset tracker in async_turn_on and async_turn_off
|
||||
self.remove_listeners = []
|
||||
|
|
@ -566,7 +629,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self) -> Optional[bool]:
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if adaptive lighting is on."""
|
||||
return self._state
|
||||
|
||||
|
|
@ -632,7 +695,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
return self._icon
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Dict[str, Any]:
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the attributes of the switch."""
|
||||
if not self.is_on:
|
||||
return {key: None for key in self._settings}
|
||||
|
|
@ -643,7 +706,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
]
|
||||
return dict(self._settings, manual_control=manual_control)
|
||||
|
||||
def create_context(self, which: str = "default") -> Context:
|
||||
def create_context(
|
||||
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:*************'
|
||||
|
|
@ -655,7 +720,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
# 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)
|
||||
context = create_context(self._name, which, self._context_cnt, parent=parent)
|
||||
self._context_cnt += 1
|
||||
return context
|
||||
|
||||
|
|
@ -688,18 +753,20 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
async def _async_update_at_interval(self, now=None) -> None:
|
||||
await self._update_attrs_and_maybe_adapt_lights(
|
||||
force=False, context=self.create_context("interval")
|
||||
transition=self._transition,
|
||||
force=False,
|
||||
context=self.create_context("interval"),
|
||||
)
|
||||
|
||||
async def _adapt_light(
|
||||
self,
|
||||
light: str,
|
||||
transition: Optional[int] = None,
|
||||
adapt_brightness: Optional[bool] = None,
|
||||
adapt_color: Optional[bool] = None,
|
||||
prefer_rgb_color: Optional[bool] = None,
|
||||
transition: int | None = None,
|
||||
adapt_brightness: bool | None = None,
|
||||
adapt_color: bool | None = None,
|
||||
prefer_rgb_color: bool | None = None,
|
||||
force: bool = False,
|
||||
context: Optional[Context] = None,
|
||||
context: Context | None = None,
|
||||
) -> None:
|
||||
lock = self._locks.get(light)
|
||||
if lock is not None and lock.locked():
|
||||
|
|
@ -724,10 +791,6 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
brightness = round(255 * self._settings["brightness_pct"] / 100)
|
||||
service_data[ATTR_BRIGHTNESS] = brightness
|
||||
|
||||
if "white_value" in features and adapt_brightness:
|
||||
white_value = round(255 * self._settings["brightness_pct"] / 100)
|
||||
service_data[ATTR_WHITE_VALUE] = white_value
|
||||
|
||||
if (
|
||||
"color_temp" in features
|
||||
and adapt_color
|
||||
|
|
@ -747,6 +810,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
and self._detect_non_ha_changes
|
||||
and not force
|
||||
and await self.turn_on_off_listener.significant_change(
|
||||
self,
|
||||
light,
|
||||
adapt_brightness,
|
||||
adapt_color,
|
||||
|
|
@ -754,23 +818,16 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
)
|
||||
):
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"%s: Scheduling 'light.turn_on' with the following 'service_data': %s"
|
||||
" with context.id='%s'",
|
||||
self._name,
|
||||
service_data,
|
||||
context.id,
|
||||
)
|
||||
self.turn_on_off_listener.last_service_data[light] = service_data
|
||||
if self._separate_turn_on_commands:
|
||||
service_datas = [
|
||||
{ATTR_ENTITY_ID: light, key: value}
|
||||
for key, value in service_data.items()
|
||||
if key != ATTR_ENTITY_ID
|
||||
]
|
||||
else:
|
||||
service_datas = [service_data]
|
||||
for service_data in service_datas:
|
||||
|
||||
async def turn_on(service_data):
|
||||
_LOGGER.debug(
|
||||
"%s: Scheduling 'light.turn_on' with the following 'service_data': %s"
|
||||
" with context.id='%s'",
|
||||
self._name,
|
||||
service_data,
|
||||
context.id,
|
||||
)
|
||||
await self.hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
|
|
@ -778,12 +835,26 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
context=context,
|
||||
)
|
||||
|
||||
if not self._separate_turn_on_commands:
|
||||
await turn_on(service_data)
|
||||
else:
|
||||
# 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 turn_on(service_datas[1])
|
||||
|
||||
async def _update_attrs_and_maybe_adapt_lights(
|
||||
self,
|
||||
lights: Optional[List[str]] = None,
|
||||
transition: Optional[int] = None,
|
||||
lights: list[str] | None = None,
|
||||
transition: int | None = None,
|
||||
force: bool = False,
|
||||
context: Optional[Context] = None,
|
||||
context: Context | None = None,
|
||||
) -> None:
|
||||
assert context is not None
|
||||
_LOGGER.debug(
|
||||
|
|
@ -793,7 +864,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
)
|
||||
assert self.is_on
|
||||
self._settings = self._sun_light_settings.get_settings(
|
||||
self.sleep_mode_switch.is_on
|
||||
self.sleep_mode_switch.is_on, transition
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
if lights is None:
|
||||
|
|
@ -804,10 +875,10 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
async def _adapt_lights(
|
||||
self,
|
||||
lights: List[str],
|
||||
transition: Optional[int],
|
||||
lights: list[str],
|
||||
transition: int | None,
|
||||
force: bool,
|
||||
context: Optional[Context],
|
||||
context: Context | None,
|
||||
) -> None:
|
||||
assert context is not None
|
||||
_LOGGER.debug(
|
||||
|
|
@ -824,6 +895,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
if (
|
||||
self._take_over_control
|
||||
and self.turn_on_off_listener.is_manually_controlled(
|
||||
self,
|
||||
light,
|
||||
force,
|
||||
self.adapt_brightness_switch.is_on,
|
||||
|
|
@ -841,6 +913,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
async def _sleep_mode_switch_state_event(self, event: Event) -> None:
|
||||
if not match_switch_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
|
||||
|
|
@ -848,9 +921,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
# Reset the manually controlled status when the "sleep mode" changes
|
||||
self.turn_on_off_listener.reset(*self._lights)
|
||||
await self._update_attrs_and_maybe_adapt_lights(
|
||||
transition=self._initial_transition,
|
||||
transition=self._sleep_transition,
|
||||
force=True,
|
||||
context=self.create_context("sleep"),
|
||||
context=self.create_context("sleep", parent=event.context),
|
||||
)
|
||||
|
||||
async def _light_event(self, event: Event) -> None:
|
||||
|
|
@ -891,7 +964,7 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
lights=[entity_id],
|
||||
transition=self._initial_transition,
|
||||
force=True,
|
||||
context=self.create_context("light_event"),
|
||||
context=self.create_context("light_event", parent=event.context),
|
||||
)
|
||||
elif (
|
||||
old_state is not None
|
||||
|
|
@ -937,7 +1010,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
|
|||
return self._icon
|
||||
|
||||
@property
|
||||
def is_on(self) -> Optional[bool]:
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if adaptive lighting is on."""
|
||||
return self._state
|
||||
|
||||
|
|
@ -954,10 +1027,12 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
|
|||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn on adaptive lighting sleep mode."""
|
||||
_LOGGER.debug("%s: Turning on", self._name)
|
||||
self._state = True
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn off adaptive lighting sleep mode."""
|
||||
_LOGGER.debug("%s: Turning off", self._name)
|
||||
self._state = False
|
||||
|
||||
|
||||
|
|
@ -973,21 +1048,41 @@ class SunLightSettings:
|
|||
min_color_temp: int
|
||||
sleep_brightness: int
|
||||
sleep_color_temp: int
|
||||
sunrise_offset: Optional[datetime.timedelta]
|
||||
sunrise_time: Optional[datetime.time]
|
||||
sunset_offset: Optional[datetime.timedelta]
|
||||
sunset_time: Optional[datetime.time]
|
||||
sunrise_offset: datetime.timedelta | None
|
||||
sunrise_time: datetime.time | None
|
||||
sunset_offset: datetime.timedelta | None
|
||||
sunset_time: datetime.time | None
|
||||
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) -> dict[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)
|
||||
utc_time = self.time_zone.localize(date_time).astimezone(dt_util.UTC)
|
||||
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)
|
||||
return utc_time
|
||||
|
||||
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 = (
|
||||
|
|
@ -1002,11 +1097,16 @@ class SunLightSettings:
|
|||
) + self.sunset_offset
|
||||
|
||||
if self.sunrise_time is None and self.sunset_time is None:
|
||||
solar_noon = location.solar_noon(date, local=False)
|
||||
solar_midnight = location.solar_midnight(date, local=False)
|
||||
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)
|
||||
else:
|
||||
solar_noon = sunrise + (sunset - sunrise) / 2
|
||||
solar_midnight = sunset + ((sunrise + timedelta(days=1)) - sunset) / 2
|
||||
(solar_noon, solar_midnight) = calculate_noon_and_midnight(sunset, sunrise)
|
||||
|
||||
events = [
|
||||
(SUN_EVENT_SUNRISE, sunrise.timestamp()),
|
||||
|
|
@ -1029,7 +1129,7 @@ class SunLightSettings:
|
|||
|
||||
return events
|
||||
|
||||
def relevant_events(self, now: datetime.datetime) -> List[Tuple[str, float]]:
|
||||
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]
|
||||
|
|
@ -1039,11 +1139,13 @@ class SunLightSettings:
|
|||
i_now = bisect.bisect([ts for _, ts in events], now.timestamp())
|
||||
return events[i_now - 1 : i_now + 1]
|
||||
|
||||
def calc_percent(self) -> float:
|
||||
def calc_percent(self, transition: int) -> float:
|
||||
"""Calculate the position of the sun in %."""
|
||||
now = dt_util.utcnow()
|
||||
now_ts = now.timestamp()
|
||||
today = self.relevant_events(now)
|
||||
|
||||
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)
|
||||
|
|
@ -1051,7 +1153,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) * ((now_ts - h) / (h - x)) ** 2 + k
|
||||
percentage = (0 - k) * ((target_ts - h) / (h - x)) ** 2 + k
|
||||
return percentage
|
||||
|
||||
def calc_brightness_pct(self, percent: float, is_sleep: bool) -> float:
|
||||
|
|
@ -1074,21 +1176,25 @@ class SunLightSettings:
|
|||
return self.min_color_temp
|
||||
|
||||
def get_settings(
|
||||
self, is_sleep
|
||||
) -> Dict[str, Union[float, Tuple[float, float], Tuple[float, float, float]]]:
|
||||
self, is_sleep, transition
|
||||
) -> dict[str, float | tuple[float, float] | tuple[float, float, float]]:
|
||||
"""Get all light settings.
|
||||
|
||||
Calculating all values takes <0.5ms.
|
||||
"""
|
||||
percent = self.calc_percent()
|
||||
percent = (
|
||||
self.calc_percent(transition)
|
||||
if transition is not None
|
||||
else self.calc_percent(0)
|
||||
)
|
||||
brightness_pct = self.calc_brightness_pct(percent, is_sleep)
|
||||
color_temp_kelvin = self.calc_color_temp_kelvin(percent, is_sleep)
|
||||
color_temp_mired: float = color_temperature_kelvin_to_mired(color_temp_kelvin)
|
||||
rgb_color: Tuple[float, float, float] = color_temperature_to_rgb(
|
||||
rgb_color: tuple[float, float, float] = color_temperature_to_rgb(
|
||||
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)
|
||||
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,
|
||||
|
|
@ -1109,19 +1215,19 @@ class TurnOnOffListener:
|
|||
self.lights = set()
|
||||
|
||||
# Tracks 'light.turn_off' service calls
|
||||
self.turn_off_event: Dict[str, Event] = {}
|
||||
self.turn_off_event: dict[str, Event] = {}
|
||||
# Tracks 'light.turn_on' service calls
|
||||
self.turn_on_event: Dict[str, Event] = {}
|
||||
self.turn_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] = {}
|
||||
self.sleep_tasks: dict[str, asyncio.Task] = {}
|
||||
# Tracks which lights are manually controlled
|
||||
self.manual_control: Dict[str, bool] = {}
|
||||
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)
|
||||
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]] = {}
|
||||
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]] = {}
|
||||
self.last_service_data: dict[str, dict[str, Any]] = {}
|
||||
|
||||
# When a state is different `max_cnt_significant_changes` times in a row,
|
||||
# mark it as manually_controlled.
|
||||
|
|
@ -1151,7 +1257,7 @@ class TurnOnOffListener:
|
|||
|
||||
service = event.data[ATTR_SERVICE]
|
||||
service_data = event.data[ATTR_SERVICE_DATA]
|
||||
entity_ids = cv.ensure_list(service_data[ATTR_ENTITY_ID])
|
||||
entity_ids = cv.ensure_list_csv(service_data[ATTR_ENTITY_ID])
|
||||
|
||||
if not any(eid in self.lights for eid in entity_ids):
|
||||
return
|
||||
|
|
@ -1211,7 +1317,7 @@ class TurnOnOffListener:
|
|||
# called with a color_temp outside of its range (and HA reports the
|
||||
# incorrect 'min_mireds' and 'max_mireds', which happens e.g., for
|
||||
# Philips Hue White GU10 Bluetooth lights).
|
||||
old_state: Optional[List[State]] = self.last_state_change.get(entity_id)
|
||||
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
|
||||
|
|
@ -1230,6 +1336,7 @@ class TurnOnOffListener:
|
|||
|
||||
def is_manually_controlled(
|
||||
self,
|
||||
switch: AdaptiveSwitch,
|
||||
light: str,
|
||||
force: bool,
|
||||
adapt_brightness: bool,
|
||||
|
|
@ -1254,7 +1361,7 @@ class TurnOnOffListener:
|
|||
# Light was already on and 'light.turn_on' was not called by
|
||||
# the adaptive_lighting integration.
|
||||
manual_control = self.manual_control[light] = True
|
||||
_fire_manual_control_event(self.hass, light, turn_on_event.context)
|
||||
_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"
|
||||
" adaptive_lighting integration (context.id='%s'), the Adaptive"
|
||||
|
|
@ -1267,6 +1374,7 @@ class TurnOnOffListener:
|
|||
|
||||
async def significant_change(
|
||||
self,
|
||||
switch: AdaptiveSwitch,
|
||||
light: str,
|
||||
adapt_brightness: bool,
|
||||
adapt_color: bool,
|
||||
|
|
@ -1281,7 +1389,7 @@ class TurnOnOffListener:
|
|||
"""
|
||||
if light not in self.last_state_change:
|
||||
return False
|
||||
old_states: List[State] = self.last_state_change[light]
|
||||
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)
|
||||
compare_to = functools.partial(
|
||||
|
|
@ -1325,7 +1433,7 @@ class TurnOnOffListener:
|
|||
# N times in a row. We do this because sometimes a state changes
|
||||
# happens only *after* a new update interval has already started.
|
||||
self.manual_control[light] = True
|
||||
_fire_manual_control_event(self.hass, light, context, is_async=False)
|
||||
_fire_manual_control_event(switch, light, context, is_async=False)
|
||||
else:
|
||||
if n_changes > 1:
|
||||
_LOGGER.debug(
|
||||
|
|
@ -1339,7 +1447,7 @@ class TurnOnOffListener:
|
|||
return changed
|
||||
|
||||
async def maybe_cancel_adjusting(
|
||||
self, entity_id: str, off_to_on_event: Event, on_to_off_event: Optional[Event]
|
||||
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.
|
||||
|
||||
|
|
|
|||
49
custom_components/adaptive_lighting/translations/da.json
Normal file
49
custom_components/adaptive_lighting/translations/da.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"title": "Adaptiv Belysning",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Vælg et navn for denne Adaptive Belysning",
|
||||
"description": "Vælg et navn til denne konfiguration. Du kan køre flere konfigurationer af Adaptiv Belysning, og hver af dem kan indeholde flere lys!",
|
||||
"data": {
|
||||
"name": "Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Denne enhed er allerede konfigureret"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Adaptiv Belysnings indstillinger",
|
||||
"description": "Alle indstillinger tilhørende en Adaptiv Belysnings komponent. Indstillingernes navne svarer til YAML indstillingernes. Ingen indstillinger vises hvis du allerede har konfigureret den i YAML.",
|
||||
"data": {
|
||||
"lights": "lights: lyskilder",
|
||||
"initial_transition": "initial_transition: Hvor lang overgang når lyset går fra 'off' til 'on' eller når 'sleep_state' skiftes. (i sekunder)",
|
||||
"interval": "interval: Tid imellem opdateringer (i sekunder)",
|
||||
"max_brightness": "max_brightness: Højeste lysstyrke i cyklussen. (%)",
|
||||
"max_color_temp": "max_color_temp: Koldeste lystemperatur i cyklussen. (Kelvin)",
|
||||
"min_brightness": "min_brightness: Laveste lysstyrke i cyklussen. (%)",
|
||||
"min_color_temp": "min_color_temp: Varmeste lystemperatur i cyklussen. (Kelvin)",
|
||||
"only_once": "only_once: Juster udelukkende lysene adaptivt i øjeblikket de tændes.",
|
||||
"prefer_rgb_color": "prefer_rgb_color: Brug 'rgb_color' istedet for 'color_temp' når muligt.",
|
||||
"separate_turn_on_commands": "separate_turn_on_commands: Adskil kommandoerne for hver attribut (color, brightness, etc.) ved 'light.turn_on' (nødvendigt for bestemte lys).",
|
||||
"sleep_brightness": "sleep_brightness, Lysstyrke for Sleep Mode. (%)",
|
||||
"sleep_color_temp": "sleep_color_temp: Farvetemperatur under Sleep Mode. (Kelvin)",
|
||||
"sunrise_offset": "sunrise_offset: Hvor længe før (-) eller efter (+) at definere solopgangen i cyklussen (+/- sekunder)",
|
||||
"sunrise_time": "sunrise_time: Manuel overstyring af solopgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt din lokation. (HH:MM:SS)",
|
||||
"sunset_offset": "sunset_offset: Hvor længe før (-) eller efter (+) at definere solnedgangen i cyklussen (+/- sekunder)",
|
||||
"sunset_time": "sunset_time: Manuel overstyring af solnedgangstidspunktet, hvis 'None', bruges det egentlige tidspunkt for din lokation. (HH:MM:SS)",
|
||||
"take_over_control": "take_over_control: Hvis andet end Adaptiv Belysning kalder 'light.turn_on' på et lys der allerede er tændt, afbryd adaptering af lyset indtil at det tændes igen.",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: Registrer alle ændringer på >10% på et lys (også udenfor HA), kræver at 'take_over_control' er slået til (kalder 'homeassistant.update_entity' hvert 'interval'!)",
|
||||
"transition": "Overgangsperiode når en ændring i lyset udføres (i sekunder)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"option_error": "Ugyldig indstilling"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,4 +46,4 @@
|
|||
"option_error": "Fehlerhafte Option"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,42 +3,43 @@
|
|||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Choose a name for the Adaptive Lighting",
|
||||
"description": "Every instance can contain multiple lights!",
|
||||
"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!",
|
||||
"data": {
|
||||
"name": "Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
"already_configured": "This device is already configured"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"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": "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 go 'off' to 'on' or when 'sleep_state' changes",
|
||||
"interval": "interval, time between switch updates in seconds",
|
||||
"max_brightness": "max_brightness, in %",
|
||||
"max_color_temp": "max_color_temp, in Kelvin",
|
||||
"min_brightness": "min_brightness, in %",
|
||||
"min_color_temp": "min_color_temp, in Kelvin",
|
||||
"only_once": "only_once, only adapt the lights when turning them on",
|
||||
"prefer_rgb_color": "prefer_rgb_color, use 'rgb_color' over 'color_temp' when possible",
|
||||
"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, in %",
|
||||
"sleep_color_temp": "sleep_color_temp, in Kelvin",
|
||||
"sunrise_offset": "sunrise_offset, in +/- seconds",
|
||||
"sunrise_time": "sunrise_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunrise time at your location)",
|
||||
"sunset_offset": "sunset_offset, in +/- seconds",
|
||||
"sunset_time": "sunset_time, in 'HH:MM:SS' format (if 'None', it uses the actual sunset time at your location)",
|
||||
"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, in seconds"
|
||||
"initial_transition": "initial_transition: When lights turn 'off' to 'on'. (seconds)",
|
||||
"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).",
|
||||
"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 (+/- 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)",
|
||||
"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 sunrise 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)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
49
custom_components/adaptive_lighting/translations/et.json
Normal file
49
custom_components/adaptive_lighting/translations/et.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"title": "Kohanduv valgus",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Vali kohanduva valguse üksuse nimi",
|
||||
"description": "Igas üksuses võib olla mitu valgustit!",
|
||||
"data": {
|
||||
"name": "Nimi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Üksus on juba seadistatud"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Kohanduva valguse suvandid",
|
||||
"description": "Kohanduva valguse suvandid. Valikute nimetused ühtuvad YAML kirjes olevatega. Valikuid ei kuvata kui seadistus on tehtud YAML kirjes.",
|
||||
"data": {
|
||||
"lights": "valgustid",
|
||||
"initial_transition": "Algne üleminek kui valgustid lülituvad sisse/välja või unerežiim muutub",
|
||||
"interval": "Intervall, aeg muutuste vahel sekundites",
|
||||
"max_brightness": "Suurim heledus %",
|
||||
"max_color_temp": "Suurim värvustemperatuur Kelvinites",
|
||||
"min_brightness": "Vähim heledus %",
|
||||
"min_color_temp": "Vähim värvustemperatuur Kelvinites",
|
||||
"only_once": "Ainult üks kord, rakendub ainult valgusti sisselülitamisel",
|
||||
"prefer_rgb_color": "Eelista RGB värve, võimalusel kasuta RGB sätteid värvustemperatuuri asemel",
|
||||
"separate_turn_on_commands": "Eraldi lülitused iga valiku (värvus, heledus jne.) sisselülitamiseks, mõned valgustid vajavad seda.",
|
||||
"sleep_brightness": "Unerežiimi heledus %",
|
||||
"sleep_color_temp": "Uneržiimi värvus Kelvinites",
|
||||
"sunrise_offset": "Nihe päikesetõusust, +/- sekundit",
|
||||
"sunrise_time": "Päikesetõusu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)",
|
||||
"sunset_offset": "Nihe päikeseloojangust, +/- sekundit",
|
||||
"sunset_time": "Päikeseloojangu aeg 'HH:MM:SS' vormingus. (Kui jätta tühjaks kasutatakse asukohajärgset)",
|
||||
"take_over_control": "Käsitsi juhtimine: kui miski peale kohanduva valguse enda lültiab valgusti sisse ja see juba põleb, katkesta kohandamine kuni järgmise välise lülitamiseni.",
|
||||
"detect_non_ha_changes": "Märka väliseid lülitusi: kui mõni säte muutub üle 10% (isegi väljaspoolt HA juhituna) siis peab käsitsi juhtimine olema lubatud (kutsutakse 'homeassistant.update_entity')'interval'!)",
|
||||
"transition": "Üleminekud, sekundites"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"option_error": "Vigane suvand"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
custom_components/adaptive_lighting/translations/fr.json
Normal file
50
custom_components/adaptive_lighting/translations/fr.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
49
custom_components/adaptive_lighting/translations/nb.json
Normal file
49
custom_components/adaptive_lighting/translations/nb.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"title":"Adaptiv Belysning",
|
||||
"config":{
|
||||
"step":{
|
||||
"user":{
|
||||
"title":"Velg et navn",
|
||||
"description":"Velg et navn for denne konfigurasjonen for adaptiv belysning - hver konfigurasjon kan inneholde flere lyskilder!",
|
||||
"data":{
|
||||
"name":"Navn"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort":{
|
||||
"already_configured":"Denne enheten er allerede konfigurert!"
|
||||
}
|
||||
},
|
||||
"options":{
|
||||
"step":{
|
||||
"init":{
|
||||
"title":"Adaptiv Belysning Innstillinger",
|
||||
"description":"Alle innstillinger for en adaptiv belysning konfigurasjon. Innstillingene er identiske med innstillingene for YAML konfigurasjon. Ingen innstillinger vises dersom du har definert adaptive_lighting i din YAML konfigurasjon.",
|
||||
"data":{
|
||||
"lights":"Lys / Lyskilder",
|
||||
"initial_transition":"'initial_transition': overgangen (i sekunder) når lysene skrus av eller på - eller når 'sleep_state' endres",
|
||||
"interval":"'interval': tiden mellom oppdateringer (i sekunder)",
|
||||
"max_brightness":"'max_brightness': den høyeste lysstyrken (i prosent) på lysene i løpet av en syklus",
|
||||
"max_color_temp":"'max_color_temp': den høyeste fargetemperaturen (i kelvin) på lysene i løpet av en syklus",
|
||||
"min_brightness":"'min_brightness': den laveste lysstyrken (i prosent) på lysene i løpet av en syklus",
|
||||
"min_color_temp":"'min_color_temp': den laveste fargetemperaturen (i kelvin) på lysene i løpet av en syklus",
|
||||
"only_once":"'only_once': anvend innstillingene for adaptiv belysning kun når lysene skrus av eller på",
|
||||
"prefer_rgb_color":"'prefer_rgb_color': benytt rgb i stedet for fargetemperatur dersom det er mulig",
|
||||
"separate_turn_on_commands":"'separate_turn_on_commands': separer kommandone i 'light.turn_on' for hver attributt (farge, lysstyrke, osv.). Dette kan være nødvendig for enkelte typer lys / lyskilder",
|
||||
"sleep_brightness":"'sleep_brightness': lysstyrken på lysene (i prosent) når 'sleep_mode' (søvnmodus) er aktiv",
|
||||
"sleep_color_temp":"'sleep_color_temp': fargetemperaturen på lysene (i kelvin) når 'sleep_mode' (søvnmodus) er aktiv",
|
||||
"sunrise_offset":"'sunrise_offset': hvor lenge før (-) eller etter (+) tidspunktet solen står opp (lokalt) skal defineres som soloppgang (i sekunder)",
|
||||
"sunrise_time":"'sunrise_time': definer tidspunktet for soloppgang manuelt (i følgende format: TT:MM:SS)",
|
||||
"sunset_offset":"'sunset_offset': hvor lenge før (-) eller etter (+) tidspunktet solen går ned (lokalt) skal defineres som solnedgang (i sekunder)",
|
||||
"sunset_time":"'sunset_time': definer tidspunktet for solnedgang manuelt (i følgende format: TT:MM:SS - f. eks: '20:30:00' vil definere tidspunktet for solnegang som halv-ni på kvelden)",
|
||||
"take_over_control":"'take_over_control': dersom en annen tjeneste enn adaptiv belysning skrur lysene av eller på, vil automatisk adaptering av lyset stoppes inntil lyset (eller den tilhørende bryteren for adaptiv belysning) blir slått av - og på igjen",
|
||||
"detect_non_ha_changes":"'detect_non_ha_changes': registrerer alle endringer i lysstyrke over 10% med opprinnelse utenfor Home Assistant - krever at 'take_over_control' er aktivert (OBS: tilkaller 'homeassistant.update_entity' ved hvert 'interval'!)",
|
||||
"transition":"'transition': varigheten (i sekunder) på overgangen når lysene oppdateres "
|
||||
}
|
||||
}
|
||||
},
|
||||
"error":{
|
||||
"option_error":"En eller flere valgte innstillinger er ugyldige"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
custom_components/adaptive_lighting/translations/pl.json
Normal file
50
custom_components/adaptive_lighting/translations/pl.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"title": "Adaptacyjne oświetlenie",
|
||||
"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ł!",
|
||||
"data": {
|
||||
"name": "Nazwa"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Już skonfigurowane!"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"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.",
|
||||
"data": {
|
||||
"lights": "światła",
|
||||
"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).",
|
||||
"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 sunrise 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)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"option_error": "Błędne opcje"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
custom_components/adaptive_lighting/translations/pt-BR.json
Normal file
50
custom_components/adaptive_lighting/translations/pt-BR.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"title": "Iluminação Adaptativa",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Escolha um nome para a instância da Iluminação Adaptativa",
|
||||
"description": "Escolha um nome para esta instância. Você pode executar várias instâncias de iluminação adaptativa, cada uma delas pode conter várias luzes!",
|
||||
"data": {
|
||||
"name": "Nome"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Este dispositivo já está configurado"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Opções da iluminação adaptiva",
|
||||
"description": "Todas as configurações de um componente de iluminação adaptativa. Os nomes das opções correspondem às configurações de YAML. Nenhuma opção será exibida se você tiver a entrada adaptive_lighting definida em sua configuração YAML.",
|
||||
"data": {
|
||||
"lights": "luzes",
|
||||
"initial_transition": "initial_transition: Quando as luzes mudam de 'off' para 'on'. (segundos)",
|
||||
"sleep_transition": "sleep_transition: Quando 'sleep_state' muda. (segundos)",
|
||||
"interval": "interval: Tempo entre as atualizações do switch. (segundos)",
|
||||
"max_brightness": "max_brightness: Maior brilho das luzes durante um ciclo. (%)",
|
||||
"max_color_temp": "max_color_temp: Matiz mais frio do ciclo de temperatura de cor. (Kelvin)",
|
||||
"min_brightness": "min_brightness: Menor brilho das luzes durante um ciclo. (%)",
|
||||
"min_color_temp": "min_color_temp, matiz mais quente do ciclo de temperatura de cor. (Kelvin)",
|
||||
"only_once": "only_once: Apenas adapte as luzes ao ligá-las.",
|
||||
"prefer_rgb_color": "prefer_rgb_color: Use 'rgb_color' em vez de 'color_temp' quando possível.",
|
||||
"separate_turn_on_commands": "separar_turn_on_commands: Separe os comandos para cada atributo (cor, brilho, etc.) em 'light.turn_on' (necessário para algumas luzes).",
|
||||
"sleep_brightness": "sleep_brightness, configuração de brilho para o modo de suspensão. (%)",
|
||||
"sleep_color_temp": "sleep_color_temp: configuração de temperatura de cor para o modo de suspensão. (Kelvin)",
|
||||
"sunrise_offset": "sunrise_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto do nascer do sol do ciclo (+/- segundos)",
|
||||
"sunrise_time": "sunrise_time: substituição manual do horário do nascer do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)",
|
||||
"sunset_offset": "Sunset_offset: Quanto tempo antes (-) ou depois (+) para definir o ponto de pôr do sol do ciclo (+/- segundos)",
|
||||
"sunset_time": "sunset_time: substituição manual do horário do pôr do sol, se 'Nenhum', ele usa o horário real do nascer do sol em sua localização (HH:MM:SS)",
|
||||
"take_over_control": "take_over_control: Se qualquer coisa, exceto Adaptive Lighting, chamar 'light.turn_on' quando uma luz já estiver acesa, pare de adaptar essa luz até que ela (ou o interruptor) desligue -> ligue.",
|
||||
"detect_non_ha_changes": "detect_non_ha_changes: detecta todas as alterações > 10% feitas nas luzes (também fora do HA), requer que 'take_over_control' seja ativado (chama 'homeassistant.update_entity' a cada 'intervalo'!)",
|
||||
"transition": "Tempo de transição ao aplicar uma mudança nas luzes (segundos)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"option_error": "Opção inválida"
|
||||
}
|
||||
}
|
||||
}
|
||||
49
custom_components/adaptive_lighting/translations/uk.json
Normal file
49
custom_components/adaptive_lighting/translations/uk.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"title": "Адаптивне освітлення",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Оберіть ім’я для екземпляра адаптивного освітлення",
|
||||
"description": "Оберіть ім’я для цього екземпляра. Ви можете мати декілька екземплярів адаптивного освітлення, кожен може містити декілька приладів!",
|
||||
"data": {
|
||||
"name": "Ім’я"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Цей пристрій вже налаштовано"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Опції адаптивного освітлення",
|
||||
"description": "Всі налаштування компонента адаптивного освітлення. Назви опцій відповідають налаштуванням у YAML. Опції не відображаються, якщо ви вже визначили їх у компоненті adaptive_lighting вашої YAML-конфігурації.",
|
||||
"data": {
|
||||
"lights": "прилади",
|
||||
"initial_transition": "initial_transition: Коли прилад вимикається (off), вмикається (on), або змінює 'sleep_state'. (секунди)",
|
||||
"interval": "interval: Час між оновленнями перемикача. (секунди)",
|
||||
"max_brightness": "max_brightness: Найвища яскравість світла під час циклу. (%)",
|
||||
"max_color_temp": "max_color_temp: Найхолодніший відтінок циклу кольорової температури. (Кельвін)",
|
||||
"min_brightness": "min_brightness: Найнижча яскравість світла під час циклу. (%)",
|
||||
"min_color_temp": "min_color_temp: Найтепліший відтінок циклу кольорової температури. (%)",
|
||||
"only_once": "only_once: Адаптувати світло лише після початкового увімкнення.",
|
||||
"prefer_rgb_color": "prefer_rgb_color: Використовувати 'rgb_color' замість 'color_temp', коли можливо.",
|
||||
"separate_turn_on_commands": "separate_turn_on_commands: Окремі команди для кожного атрибута (колір, яскравість, тощо.) в 'light.turn_on' (необхідні для деяких приладів).",
|
||||
"sleep_brightness": "sleep_brightness: Налаштування яскравості для Режиму сну. (%)",
|
||||
"sleep_color_temp": "sleep_color_temp: Температура кольору для Режиму сну. (Кельвін)",
|
||||
"sunrise_offset": "sunrise_offset: Як за довго до(-) або після(+) визначати точку сходу сонця для циклу (+/- секунд)",
|
||||
"sunrise_time": "sunrise_time: Ручний перезапис часу сходу сонця, якщо 'None', тоді використовується час сходу сонця у вашій локації (HH:MM:SS)",
|
||||
"sunset_offset": "sunset_offset: Як за довго до(-) або після(+) визначати точку заходу сонця для циклу (+/- секунд)",
|
||||
"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": "Час переходу, який застосовується до освітлення (секунди)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"option_error": "Хибна опція"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"name": "adaptive_lighting",
|
||||
"render_readme": true,
|
||||
"domains": ["switch"]
|
||||
"name": "Adaptive Lighting",
|
||||
"render_readme": true
|
||||
}
|
||||
|
|
|
|||
7
info.md
7
info.md
|
|
@ -1,7 +0,0 @@
|
|||
## Stay healthier and sleep better by syncing your lights with natural daylight to maintain your circadian rhythm!
|
||||
|
||||
<img src="https://community-home-assistant-assets.s3.dualstack.us-west-2.amazonaws.com/original/3X/5/f/5fe7a780e9f8905fea4d1cbb66cdbe35858a6e36.jpg" width="690px">
|
||||
|
||||
Circadian Lighting slowly synchronizes your color changing lights with the regular naturally occurring color temperature of the sky throughout the day. This gives your environment a more natural feel, with cooler hues during the midday and warmer tints near twilight and dawn.
|
||||
|
||||
In addition, Circadian Lighting can set your lights to a nice cool white at 1% in “Sleep” mode, which is far brighter than starlight but won’t reset your circadian rhythm or break down too much rhodopsin in your eyes.
|
||||
11
setup.cfg
Normal file
11
setup.cfg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[isort]
|
||||
force_sort_within_sections=True
|
||||
profile=black
|
||||
|
||||
[flake8]
|
||||
ignore = E203, E266, W503
|
||||
max-line-length = 100
|
||||
max-complexity = 18
|
||||
select = B,C,E,F,W,T4,B9
|
||||
per-file-ignores =
|
||||
code_example.py: E402, E501
|
||||
30
test_dependencies.py
Normal file
30
test_dependencies.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
with open("core/requirements_test_all.txt") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
components = []
|
||||
packages = []
|
||||
deps = {}
|
||||
for i, line in enumerate(lines):
|
||||
line = line.strip()
|
||||
if line.startswith("# homeassistant."):
|
||||
component = line.split("# homeassistant.")[1]
|
||||
components.append(component)
|
||||
elif components and line:
|
||||
packages.append(line)
|
||||
else:
|
||||
for component in components:
|
||||
for package in packages:
|
||||
deps.setdefault(component, []).append(package)
|
||||
components = []
|
||||
packages = []
|
||||
|
||||
required = [
|
||||
"components.recorder",
|
||||
"components.mqtt",
|
||||
"components.zeroconf",
|
||||
"components.http",
|
||||
]
|
||||
to_install = []
|
||||
for r in required:
|
||||
to_install.extend(deps[r])
|
||||
print(" ".join(to_install))
|
||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Adaptive Lighting integration."""
|
||||
131
tests/test_config_flow.py
Normal file
131
tests/test_config_flow.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"""Test Adaptive Lighting config flow."""
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.adaptive_lighting.const import (
|
||||
CONF_SUNRISE_TIME,
|
||||
CONF_SUNSET_TIME,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
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}
|
||||
|
||||
|
||||
async def test_flow_manual_configuration(hass):
|
||||
"""Test that config flow works."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "user"}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["handler"] == "adaptive_lighting"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_NAME: "living room"}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "living room"
|
||||
|
||||
|
||||
async def test_import_success(hass):
|
||||
"""Test import step is successful."""
|
||||
data = DEFAULT_DATA.copy()
|
||||
data[CONF_NAME] = DEFAULT_NAME
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": "import"},
|
||||
data=data,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == DEFAULT_NAME
|
||||
for key, value in data.items():
|
||||
assert result["data"][key] == value
|
||||
|
||||
|
||||
async def test_options(hass):
|
||||
"""Test updating options."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=DEFAULT_NAME,
|
||||
data={CONF_NAME: DEFAULT_NAME},
|
||||
options={},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
data = DEFAULT_DATA.copy()
|
||||
data[CONF_SUNRISE_TIME] = NONE_STR
|
||||
data[CONF_SUNSET_TIME] = NONE_STR
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=data,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
for key, value in data.items():
|
||||
assert result["data"][key] == value
|
||||
|
||||
|
||||
async def test_incorrect_options(hass):
|
||||
"""Test updating incorrect options."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=DEFAULT_NAME,
|
||||
data={CONF_NAME: DEFAULT_NAME},
|
||||
options={},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
data = DEFAULT_DATA.copy()
|
||||
data[CONF_SUNRISE_TIME] = "yolo"
|
||||
data[CONF_SUNSET_TIME] = "yolo"
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=data,
|
||||
)
|
||||
|
||||
|
||||
async def test_import_twice(hass):
|
||||
"""Test importing twice."""
|
||||
data = DEFAULT_DATA.copy()
|
||||
data[CONF_NAME] = DEFAULT_NAME
|
||||
for _ in range(2):
|
||||
_ = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": "import"},
|
||||
data=data,
|
||||
)
|
||||
|
||||
|
||||
async def test_changing_options_when_using_yaml(hass):
|
||||
"""Test changing options when using YAML."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=DEFAULT_NAME,
|
||||
data={CONF_NAME: DEFAULT_NAME},
|
||||
source=SOURCE_IMPORT,
|
||||
options={},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
55
tests/test_init.py
Normal file
55
tests/test_init.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""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 tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_with_config(hass):
|
||||
"""Test that we import the config and setup the integration."""
|
||||
config = {
|
||||
adaptive_lighting.DOMAIN: {
|
||||
adaptive_lighting.CONF_NAME: DEFAULT_NAME,
|
||||
}
|
||||
}
|
||||
assert await async_setup_component(hass, adaptive_lighting.DOMAIN, config)
|
||||
assert adaptive_lighting.DOMAIN in hass.data
|
||||
|
||||
|
||||
async def test_successful_config_entry(hass):
|
||||
"""Test that Adaptive Lighting is configured successfully."""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=adaptive_lighting.DOMAIN,
|
||||
data={CONF_NAME: DEFAULT_NAME},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
assert entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert UNDO_UPDATE_LISTENER in hass.data[adaptive_lighting.DOMAIN][entry.entry_id]
|
||||
|
||||
|
||||
async def test_unload_entry(hass):
|
||||
"""Test removing Adaptive Lighting."""
|
||||
entry = MockConfigEntry(
|
||||
domain=adaptive_lighting.DOMAIN,
|
||||
data={CONF_NAME: DEFAULT_NAME},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert adaptive_lighting.DOMAIN not in hass.data
|
||||
871
tests/test_switch.py
Normal file
871
tests/test_switch.py
Normal file
|
|
@ -0,0 +1,871 @@
|
|||
"""Tests for Adaptive Lighting switches."""
|
||||
# pylint: disable=protected-access
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
from random import randint
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.adaptive_lighting.const import (
|
||||
ADAPT_BRIGHTNESS_SWITCH,
|
||||
ADAPT_COLOR_SWITCH,
|
||||
ATTR_TURN_ON_OFF_LISTENER,
|
||||
CONF_DETECT_NON_HA_CHANGES,
|
||||
CONF_INITIAL_TRANSITION,
|
||||
CONF_MANUAL_CONTROL,
|
||||
CONF_MIN_COLOR_TEMP,
|
||||
CONF_PREFER_RGB_COLOR,
|
||||
CONF_SEPARATE_TURN_ON_COMMANDS,
|
||||
CONF_SUNRISE_OFFSET,
|
||||
CONF_SUNRISE_TIME,
|
||||
CONF_SUNSET_TIME,
|
||||
CONF_TRANSITION,
|
||||
CONF_TURN_ON_LIGHTS,
|
||||
DEFAULT_MAX_BRIGHTNESS,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_SLEEP_BRIGHTNESS,
|
||||
DEFAULT_SLEEP_COLOR_TEMP,
|
||||
DOMAIN,
|
||||
SERVICE_APPLY,
|
||||
SERVICE_SET_MANUAL_CONTROL,
|
||||
SLEEP_MODE_SWITCH,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
from homeassistant.components.adaptive_lighting.switch import (
|
||||
_attributes_have_changed,
|
||||
_expand_light_groups,
|
||||
color_difference_redmean,
|
||||
create_context,
|
||||
is_our_context,
|
||||
)
|
||||
from homeassistant.components.demo.light import DemoLight
|
||||
from homeassistant.components.group import DOMAIN as GROUP_DOMAIN
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_RGB_COLOR,
|
||||
)
|
||||
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_ENTITY_ID,
|
||||
CONF_LIGHTS,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import Context, State
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
import pytest
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.demo.test_light import ENTITY_LIGHT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUNRISE = datetime.datetime(
|
||||
year=2020,
|
||||
month=10,
|
||||
day=17,
|
||||
hour=6,
|
||||
)
|
||||
SUNSET = datetime.datetime(
|
||||
year=2020,
|
||||
month=10,
|
||||
day=17,
|
||||
hour=22,
|
||||
)
|
||||
|
||||
LAT_LONG_TZS = [
|
||||
(39, -1, "Europe/Madrid"),
|
||||
(60, 50, "GMT"),
|
||||
(55, 13, "Europe/Copenhagen"),
|
||||
(52.379189, 4.899431, "Europe/Amsterdam"),
|
||||
(32.87336, -117.22743, "US/Pacific"),
|
||||
]
|
||||
|
||||
_SWITCH_FMT = f"{SWITCH_DOMAIN}.{DOMAIN}"
|
||||
ENTITY_SWITCH = f"{_SWITCH_FMT}_{DEFAULT_NAME}"
|
||||
ENTITY_SLEEP_MODE_SWITCH = f"{_SWITCH_FMT}_sleep_mode_{DEFAULT_NAME}"
|
||||
ENTITY_ADAPT_BRIGHTNESS_SWITCH = f"{_SWITCH_FMT}_adapt_brightness_{DEFAULT_NAME}"
|
||||
ENTITY_ADAPT_COLOR_SWITCH = f"{_SWITCH_FMT}_adapt_color_{DEFAULT_NAME}"
|
||||
|
||||
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reset_time_zone():
|
||||
"""Reset time zone."""
|
||||
yield
|
||||
dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE
|
||||
|
||||
|
||||
async def setup_switch(hass, extra_data):
|
||||
"""Create the switch entry."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME, **extra_data})
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
switch = hass.data[DOMAIN][entry.entry_id][SWITCH_DOMAIN]
|
||||
return entry, switch
|
||||
|
||||
|
||||
async def setup_lights(hass):
|
||||
"""Set up 3 light entities using the 'test' platform."""
|
||||
await async_setup_component(
|
||||
hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}}
|
||||
)
|
||||
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,
|
||||
),
|
||||
]
|
||||
for light in lights:
|
||||
light.hass = hass
|
||||
slug = light.name.lower().replace(" ", "_")
|
||||
light.entity_id = f"light.{slug}"
|
||||
await light.async_update_ha_state()
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def setup_lights_and_switch(hass, extra_conf=None):
|
||||
"""Create switch and demo lights."""
|
||||
# Setup demo lights and turn on
|
||||
lights_instances = await setup_lights(hass)
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: ENTITY_LIGHT},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Setup switch
|
||||
lights = [
|
||||
"light.bed_light",
|
||||
"light.ceiling_lights",
|
||||
]
|
||||
assert all(hass.states.get(light) is not None for light in lights)
|
||||
_, switch = await setup_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_LIGHTS: lights,
|
||||
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
|
||||
**(extra_conf or {}),
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
return switch, lights_instances
|
||||
|
||||
|
||||
async def test_adaptive_lighting_switches(hass):
|
||||
"""Test switches created for adaptive_lighting integration."""
|
||||
entry, _ = await setup_switch(hass, {})
|
||||
|
||||
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4
|
||||
assert set(hass.states.async_entity_ids(SWITCH_DOMAIN)) == {
|
||||
ENTITY_SWITCH,
|
||||
ENTITY_SLEEP_MODE_SWITCH,
|
||||
ENTITY_ADAPT_COLOR_SWITCH,
|
||||
ENTITY_ADAPT_BRIGHTNESS_SWITCH,
|
||||
}
|
||||
assert ATTR_TURN_ON_OFF_LISTENER in hass.data[DOMAIN]
|
||||
assert entry.entry_id in hass.data[DOMAIN]
|
||||
assert len(hass.data[DOMAIN].keys()) == 2
|
||||
|
||||
data = hass.data[DOMAIN][entry.entry_id]
|
||||
assert SLEEP_MODE_SWITCH in data
|
||||
assert SWITCH_DOMAIN in data
|
||||
assert ADAPT_COLOR_SWITCH in data
|
||||
assert ADAPT_BRIGHTNESS_SWITCH in data
|
||||
assert UNDO_UPDATE_LISTENER in data
|
||||
assert len(data.keys()) == 5
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lat,long,timezone", LAT_LONG_TZS)
|
||||
async def test_adaptive_lighting_time_zones_with_default_settings(
|
||||
hass, lat, long, timezone, reset_time_zone # pylint: disable=redefined-outer-name
|
||||
):
|
||||
"""Test setting up the Adaptive Lighting switches with different timezones."""
|
||||
await config_util.async_process_ha_core_config(
|
||||
hass,
|
||||
{"latitude": lat, "longitude": long, "time_zone": timezone},
|
||||
)
|
||||
_, switch = await setup_switch(hass, {})
|
||||
# Shouldn't raise an exception ever
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
context=switch.create_context("test")
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
):
|
||||
"""Test setting up the Adaptive Lighting switches with different timezones.
|
||||
|
||||
Also test the (sleep) brightness and color temperature settings.
|
||||
"""
|
||||
await config_util.async_process_ha_core_config(
|
||||
hass,
|
||||
{"latitude": lat, "longitude": long, "time_zone": timezone},
|
||||
)
|
||||
_, switch = await setup_switch(
|
||||
hass,
|
||||
{
|
||||
CONF_SUNRISE_TIME: datetime.time(SUNRISE.hour),
|
||||
CONF_SUNSET_TIME: datetime.time(SUNSET.hour),
|
||||
},
|
||||
)
|
||||
|
||||
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 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 switch._settings[ATTR_BRIGHTNESS_PCT] == DEFAULT_MAX_BRIGHTNESS
|
||||
assert switch._settings["color_temp_kelvin"] > min_color_temp
|
||||
|
||||
# One hour after sunset the brightness should be down
|
||||
await patch_time_and_update(after_sunset)
|
||||
assert switch._settings[ATTR_BRIGHTNESS_PCT] < DEFAULT_MAX_BRIGHTNESS
|
||||
assert switch._settings["color_temp_kelvin"] == min_color_temp
|
||||
|
||||
# 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
|
||||
|
||||
# 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 switch._settings["color_temp_kelvin"] == min_color_temp
|
||||
|
||||
# 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
|
||||
|
||||
# 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["color_temp_kelvin"] == DEFAULT_SLEEP_COLOR_TEMP
|
||||
|
||||
|
||||
async def test_light_settings(hass):
|
||||
"""Test that light settings are correctly applied."""
|
||||
switch, _ = await setup_lights_and_switch(hass)
|
||||
lights = switch._lights
|
||||
|
||||
# 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()
|
||||
light_states = [hass.states.get(light) for light in lights]
|
||||
for state in light_states:
|
||||
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
|
||||
]
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == last_service_data[ATTR_BRIGHTNESS]
|
||||
assert state.attributes[ATTR_COLOR_TEMP] == last_service_data[ATTR_COLOR_TEMP]
|
||||
|
||||
# Turn off "sleep mode"
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: ENTITY_SLEEP_MODE_SWITCH},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test with different times
|
||||
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)
|
||||
|
||||
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):
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
transition=0, context=context, force=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
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
|
||||
]
|
||||
assert state.attributes[ATTR_COLOR_TEMP] == last_service_data[ATTR_COLOR_TEMP]
|
||||
|
||||
# At sunset the brightness should be max and color_temp at the smallest value
|
||||
light_states = await patch_time_and_get_updated_states(sunset)
|
||||
for state in light_states:
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == 255
|
||||
assert_expected_color_temp(state)
|
||||
|
||||
# One hour before sunset the brightness should be max and color_temp
|
||||
# not at the smallest value yet.
|
||||
light_states = await patch_time_and_get_updated_states(before_sunset)
|
||||
for state in light_states:
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == 255
|
||||
assert_expected_color_temp(state)
|
||||
|
||||
# One hour after sunset the brightness should be down
|
||||
light_states = await patch_time_and_get_updated_states(after_sunset)
|
||||
for state in light_states:
|
||||
assert state.attributes[ATTR_BRIGHTNESS] < 255
|
||||
assert_expected_color_temp(state)
|
||||
|
||||
# At sunrise the brightness should be max and color_temp at the smallest value
|
||||
light_states = await patch_time_and_get_updated_states(sunrise)
|
||||
for state in light_states:
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == 255
|
||||
assert_expected_color_temp(state)
|
||||
|
||||
# One hour before sunrise the brightness should smaller than max
|
||||
# and color_temp at the min value.
|
||||
light_states = await patch_time_and_get_updated_states(before_sunrise)
|
||||
for state in light_states:
|
||||
assert state.attributes[ATTR_BRIGHTNESS] < 255
|
||||
assert_expected_color_temp(state)
|
||||
|
||||
# One hour after sunrise the brightness should be up
|
||||
light_states = await patch_time_and_get_updated_states(after_sunrise)
|
||||
for state in light_states:
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == 255
|
||||
assert_expected_color_temp(state)
|
||||
|
||||
|
||||
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
|
||||
for state in [True, False]:
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON if state else SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: light},
|
||||
blocking=True,
|
||||
)
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
context=switch.create_context("test")
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert light not in switch.turn_on_off_listener.lights
|
||||
|
||||
|
||||
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
|
||||
|
||||
async def update():
|
||||
await switch._update_attrs_and_maybe_adapt_lights(transition=0, context=context)
|
||||
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()
|
||||
await update()
|
||||
_LOGGER.debug("Turn light %s, to %s", state, kwargs)
|
||||
|
||||
async def turn_switch(state, entity_id):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_ON if state else SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async def change_manual_control(set_to, extra_service_data=None):
|
||||
if extra_service_data is None:
|
||||
extra_service_data = {CONF_LIGHTS: [ENTITY_LIGHT]}
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_MANUAL_CONTROL,
|
||||
{
|
||||
ATTR_ENTITY_ID: switch.entity_id,
|
||||
CONF_MANUAL_CONTROL: set_to,
|
||||
**extra_service_data,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
await update()
|
||||
|
||||
def increased_brightness():
|
||||
return (light._brightness + 100) % 255
|
||||
|
||||
def increased_color_temp():
|
||||
return max((light._ct + 100) % light.max_mireds, light.min_mireds)
|
||||
|
||||
# Nothing is manually controlled
|
||||
await update()
|
||||
assert not manual_control[ENTITY_LIGHT]
|
||||
# Call light.turn_on for ENTITY_LIGHT
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
# Check that ENTITY_LIGHT is manually controlled
|
||||
assert manual_control[ENTITY_LIGHT]
|
||||
# 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 toggling light off to on resets manual control
|
||||
await change_manual_control(True)
|
||||
assert manual_control[ENTITY_LIGHT]
|
||||
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
|
||||
|
||||
# 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]
|
||||
await turn_switch(False, entity_id)
|
||||
await turn_switch(True, entity_id)
|
||||
assert not 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)
|
||||
assert not manual_control[ENTITY_LIGHT]
|
||||
await switch.adapt_brightness_switch.async_turn_off()
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
assert not manual_control[ENTITY_LIGHT]
|
||||
await turn_light(True, color_temp=(light._ct + 100) % 500)
|
||||
assert manual_control[ENTITY_LIGHT]
|
||||
await switch.adapt_brightness_switch.async_turn_on() # turn on again
|
||||
|
||||
# Check that when 'adapt_color' is off, changing the color
|
||||
# doesn't mark it as manually controlled but changing brightness
|
||||
# does
|
||||
await turn_light(False) # reset manually controlled status
|
||||
await turn_light(True)
|
||||
assert not manual_control[ENTITY_LIGHT]
|
||||
await switch.adapt_color_switch.async_turn_off()
|
||||
await turn_light(True, color_temp=increased_color_temp())
|
||||
assert not manual_control[ENTITY_LIGHT]
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
assert manual_control[ENTITY_LIGHT]
|
||||
|
||||
# Check that when 'adapt_color' adapt_brightness are both off
|
||||
# nothing marks it as manually controlled
|
||||
await turn_light(False) # reset manually controlled status
|
||||
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]
|
||||
await turn_light(True, color_temp=increased_color_temp())
|
||||
await turn_light(True, brightness=increased_brightness())
|
||||
await turn_light(
|
||||
True,
|
||||
color_temp=increased_color_temp(),
|
||||
brightness=increased_brightness(),
|
||||
)
|
||||
assert not manual_control[ENTITY_LIGHT]
|
||||
# Turn switches on again
|
||||
await switch.adapt_color_switch.async_turn_on()
|
||||
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])
|
||||
# do not pass "lights" so reset all
|
||||
await change_manual_control(False, {})
|
||||
assert all([not manual_control[eid] for eid in switch._lights])
|
||||
|
||||
|
||||
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
|
||||
|
||||
def increased_brightness():
|
||||
return (light._brightness + 100) % 255
|
||||
|
||||
def increased_color_temp():
|
||||
return max((light._ct + 100) % light.max_mireds, light.min_mireds)
|
||||
|
||||
async def change_light():
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_BRIGHTNESS: increased_brightness(),
|
||||
ATTR_COLOR_TEMP: increased_color_temp(),
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async def apply(**kwargs):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_APPLY,
|
||||
{
|
||||
ATTR_ENTITY_ID: ENTITY_SWITCH,
|
||||
CONF_LIGHTS: [entity_id],
|
||||
CONF_TURN_ON_LIGHTS: True,
|
||||
**kwargs,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Test turn on with defaults
|
||||
assert hass.states.get(entity_id).state == STATE_OFF
|
||||
await apply()
|
||||
assert hass.states.get(entity_id).state == STATE_ON
|
||||
await change_light()
|
||||
|
||||
# Test only changing color
|
||||
old_state = hass.states.get(entity_id).attributes
|
||||
await apply(adapt_color=True, adapt_brightness=False)
|
||||
new_state = hass.states.get(entity_id).attributes
|
||||
assert old_state[ATTR_BRIGHTNESS] == new_state[ATTR_BRIGHTNESS]
|
||||
assert old_state[ATTR_COLOR_TEMP] != new_state[ATTR_COLOR_TEMP]
|
||||
|
||||
# Test only changing brightness
|
||||
await change_light()
|
||||
old_state = hass.states.get(entity_id).attributes
|
||||
await apply(adapt_color=False, adapt_brightness=True)
|
||||
new_state = hass.states.get(entity_id).attributes
|
||||
assert old_state[ATTR_BRIGHTNESS] != new_state[ATTR_BRIGHTNESS]
|
||||
assert old_state[ATTR_COLOR_TEMP] == new_state[ATTR_COLOR_TEMP]
|
||||
|
||||
|
||||
async def test_switch_off_on_off(hass):
|
||||
"""Test switch rapid off_on_off."""
|
||||
|
||||
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():
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
transition=0, context=switch.create_context("test")
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
switch, _ = await setup_lights_and_switch(hass)
|
||||
|
||||
for turn_light_state_at_end in [True, False]:
|
||||
# Turn light on
|
||||
await turn_light(True)
|
||||
# Turn light off with transition
|
||||
await turn_light(False, transition=1)
|
||||
|
||||
assert not switch.turn_on_off_listener.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)
|
||||
# Set state to off after a second (like happens IRL)
|
||||
await asyncio.sleep(1e-3)
|
||||
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 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
|
||||
if turn_light_state_at_end:
|
||||
assert sleep_task.cancelled()
|
||||
assert state == STATE_ON
|
||||
else:
|
||||
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()
|
||||
|
||||
switch, (bed_light_instance, *_) = await setup_lights_and_switch(hass)
|
||||
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 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):
|
||||
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)
|
||||
assert not switch.turn_on_off_listener.manual_control[ENTITY_LIGHT]
|
||||
|
||||
|
||||
def test_color_difference_redmean():
|
||||
"""Test color_difference_redmean function."""
|
||||
for _ in range(10):
|
||||
rgb_1 = (randint(0, 255), randint(0, 255), randint(0, 255))
|
||||
rgb_2 = (randint(0, 255), randint(0, 255), randint(0, 255))
|
||||
color_difference_redmean(rgb_1, rgb_2)
|
||||
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 = {ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0), ATTR_COLOR_TEMP: 100}
|
||||
attributes_2 = {
|
||||
ATTR_BRIGHTNESS: 100,
|
||||
ATTR_RGB_COLOR: (255, 0, 0),
|
||||
ATTR_COLOR_TEMP: 300,
|
||||
}
|
||||
kwargs = dict(
|
||||
light="light.test",
|
||||
adapt_brightness=True,
|
||||
adapt_color=True,
|
||||
context=Context(),
|
||||
)
|
||||
assert not _attributes_have_changed(
|
||||
old_attributes=attributes_1, new_attributes=attributes_1, **kwargs
|
||||
)
|
||||
for key, value in attributes_2.items():
|
||||
attrs = dict(attributes_1)
|
||||
attrs[key] = value
|
||||
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: 100},
|
||||
new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (0, 0, 0)},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
async def test_unload_switch(hass):
|
||||
"""Test removing Adaptive Lighting."""
|
||||
entry, _ = await setup_switch(hass, {})
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert DOMAIN not in hass.data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state", [STATE_ON, STATE_OFF, None])
|
||||
async def test_restore_off_state(hass, state):
|
||||
"""Test that the 'off' and 'on' states are propoperly restored."""
|
||||
with patch(
|
||||
"homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state",
|
||||
return_value=State(ENTITY_SWITCH, state) if state is not None else None,
|
||||
):
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
_, switch = await setup_switch(hass, {})
|
||||
if state == STATE_ON:
|
||||
assert switch.is_on
|
||||
elif state == STATE_OFF:
|
||||
assert not switch.is_on
|
||||
elif state is None:
|
||||
assert switch.is_on
|
||||
|
||||
for _switch, initial_state in [
|
||||
(switch.sleep_mode_switch, False),
|
||||
(switch.adapt_brightness_switch, True),
|
||||
(switch.adapt_color_switch, True),
|
||||
]:
|
||||
if state == STATE_ON:
|
||||
assert _switch.is_on
|
||||
elif state == STATE_OFF:
|
||||
assert not _switch.is_on
|
||||
elif state is None:
|
||||
if initial_state:
|
||||
assert _switch.is_on
|
||||
else:
|
||||
assert not _switch.is_on
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="Offset is larger than half a day")
|
||||
async def test_offset_too_large(hass):
|
||||
"""Test that update fails when the offset is too large."""
|
||||
_, switch = await setup_switch(hass, {CONF_SUNRISE_OFFSET: 3600 * 12})
|
||||
await switch._update_attrs_and_maybe_adapt_lights(
|
||||
context=switch.create_context("test")
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_turn_on_and_off_when_already_at_that_state(hass):
|
||||
"""Test 'switch.turn_on/off' when switch is on/off."""
|
||||
_, switch = await setup_switch(hass, {})
|
||||
|
||||
await switch.async_turn_on()
|
||||
await hass.async_block_till_done()
|
||||
await switch.async_turn_on()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await switch.async_turn_off()
|
||||
await hass.async_block_till_done()
|
||||
await switch.async_turn_off()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_async_update_at_interval(hass):
|
||||
"""Test '_async_update_at_interval' method."""
|
||||
_, switch = await setup_switch(hass, {})
|
||||
await switch._async_update_at_interval()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("separate_turn_on_commands", (True, False))
|
||||
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(
|
||||
hass, {CONF_SEPARATE_TURN_ON_COMMANDS: separate_turn_on_commands}
|
||||
)
|
||||
# We just turn sleep mode on and off which should change the
|
||||
# brightness and color. We don't test whether the number are exactly
|
||||
# what we expect because we do this in other tests already, we merely
|
||||
# check whether the brightness and color_temp change.
|
||||
context = switch.create_context("test") # needs to be passed to update method
|
||||
brightness = light.brightness
|
||||
color_temp = light.color_temp
|
||||
await switch.sleep_mode_switch.async_turn_on()
|
||||
await switch._update_attrs_and_maybe_adapt_lights(context=context)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# TODO: figure out why `light.brightness` is not updating
|
||||
attrs = hass.states.get(light.entity_id).attributes
|
||||
sleep_brightness = attrs["brightness"]
|
||||
sleep_color_temp = attrs["color_temp"]
|
||||
|
||||
assert sleep_brightness != brightness
|
||||
assert sleep_color_temp != color_temp
|
||||
|
||||
await switch.sleep_mode_switch.async_turn_off()
|
||||
await switch._update_attrs_and_maybe_adapt_lights(context=context)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
attrs = hass.states.get(light.entity_id).attributes
|
||||
brightness = attrs["brightness"]
|
||||
color_temp = attrs["color_temp"]
|
||||
|
||||
assert sleep_brightness != brightness
|
||||
assert sleep_color_temp != color_temp
|
||||
Loading…
Add table
Add a link
Reference in a new issue