mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-15 08:14:04 +02:00
Merge remote-tracking branch 'origin/main' into external-from-kelvin-to-rgb-manual
This commit is contained in:
commit
537373987a
23 changed files with 223 additions and 42 deletions
|
|
@ -1062,6 +1062,24 @@
|
|||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Tommatheussen",
|
||||
"name": "Tom Matheussen",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13683094?v=4",
|
||||
"profile": "https://github.com/Tommatheussen",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ams2990",
|
||||
"name": "ams2990",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/488907?v=4",
|
||||
"profile": "https://github.com/ams2990",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "basnijholt/adaptive_lighting",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.13",
|
||||
"image": "mcr.microsoft.com/devcontainers/python:3-3.13",
|
||||
"postCreateCommand": "./scripts/setup-devcontainer && . .venv/bin/activate",
|
||||
"forwardPorts": [
|
||||
8123
|
||||
|
|
|
|||
6
.github/workflows/deploy-webapp.yml
vendored
6
.github/workflows/deploy-webapp.yml
vendored
|
|
@ -30,10 +30,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.13.5
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
# Upload the 'site' directory, where your app has been built
|
||||
path: "site"
|
||||
|
|
|
|||
2
.github/workflows/hassfest.yaml
vendored
2
.github/workflows/hassfest.yaml
vendored
|
|
@ -11,5 +11,5 @@ jobs:
|
|||
validate_hassfest:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4.2.2"
|
||||
- uses: "actions/checkout@v6.0.0"
|
||||
- uses: home-assistant/actions/hassfest@master
|
||||
|
|
|
|||
|
|
@ -14,25 +14,25 @@ runs:
|
|||
using: "composite"
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ github.ref }}
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: home-assistant/core
|
||||
path: core
|
||||
ref: ${{ inputs.core-version }}
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
uses: actions/setup-python@v6.1.0
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Set up UV
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
|
|||
2
.github/workflows/main-to-master-sync.yml
vendored
2
.github/workflows/main-to-master-sync.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
|
|
|||
4
.github/workflows/pre-commit.yaml
vendored
4
.github/workflows/pre-commit.yaml
vendored
|
|
@ -9,6 +9,6 @@ jobs:
|
|||
pre-commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
|
|
|
|||
14
.github/workflows/pytest.yaml
vendored
14
.github/workflows/pytest.yaml
vendored
|
|
@ -26,13 +26,23 @@ jobs:
|
|||
python-version: "3.13"
|
||||
- core-version: "2025.5.3"
|
||||
python-version: "3.13"
|
||||
- core-version: "2025.6.1"
|
||||
- core-version: "2025.6.3"
|
||||
python-version: "3.13"
|
||||
- core-version: "2025.7.4"
|
||||
python-version: "3.13"
|
||||
- core-version: "2025.8.3"
|
||||
python-version: "3.13"
|
||||
- core-version: "2025.9.4"
|
||||
python-version: "3.13"
|
||||
- core-version: "2025.10.4"
|
||||
python-version: "3.13"
|
||||
- core-version: "2025.11.3"
|
||||
python-version: "3.13"
|
||||
- core-version: "dev"
|
||||
python-version: "3.13"
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Home Assistant
|
||||
uses: ./.github/workflows/install_dependencies
|
||||
|
|
|
|||
2
.github/workflows/update-readme.yml
vendored
2
.github/workflows/update-readme.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Home Assistant
|
||||
uses: ./.github/workflows/install_dependencies
|
||||
|
|
|
|||
2
.github/workflows/validate.yml
vendored
2
.github/workflows/validate.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
validate_hacs:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: "actions/checkout@v6"
|
||||
- name: HACS validation
|
||||
uses: "hacs/action@main"
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@
|
|||
|
||||
FROM ghcr.io/astral-sh/uv:debian
|
||||
|
||||
# Install build dependencies for Python extensions
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3-dev \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Clone home-assistant/core
|
||||
RUN git clone --depth 1 --branch dev https://github.com/home-assistant/core.git /core
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[](https://github.com/hacs/integration)
|
||||

|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
# 🌞 Adaptive Lighting: Enhance Your Home's Atmosphere with Smart, Sun-Synchronized Lighting 🌙
|
||||
|
|
@ -616,6 +616,8 @@ Notice the values of `brightness_mode_time_light` and `brightness_mode_time_dark
|
|||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bittin"><img src="https://avatars.githubusercontent.com/u/43197?v=4?s=100" width="100px;" alt="Luna Jernberg"/><br /><sub><b>Luna Jernberg</b></sub></a><br /><a href="#translation-bittin" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://jeffalwilson.com"><img src="https://avatars.githubusercontent.com/u/1368827?v=4?s=100" width="100px;" alt="Jeff Wilson"/><br /><sub><b>Jeff Wilson</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=jawilson" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TermeHansen"><img src="https://avatars.githubusercontent.com/u/6922018?v=4?s=100" width="100px;" alt="Rasmus Lundsgaard"/><br /><sub><b>Rasmus Lundsgaard</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=TermeHansen" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tommatheussen"><img src="https://avatars.githubusercontent.com/u/13683094?v=4?s=100" width="100px;" alt="Tom Matheussen"/><br /><sub><b>Tom Matheussen</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=Tommatheussen" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ams2990"><img src="https://avatars.githubusercontent.com/u/488907?v=4?s=100" width="100px;" alt="ams2990"/><br /><sub><b>ams2990</b></sub></a><br /><a href="https://github.com/basnijholt/adaptive-lighting/commits?author=ams2990" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
|
|
|
|||
|
|
@ -70,12 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|||
return True
|
||||
|
||||
|
||||
async def async_update_options(hass, config_entry: ConfigEntry):
|
||||
async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry):
|
||||
"""Update options."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass, config_entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_forward_entry_unload(
|
||||
config_entry,
|
||||
|
|
|
|||
|
|
@ -57,8 +57,6 @@ def _type_to_str(type_: Any) -> str: # noqa: PLR0911
|
|||
|
||||
|
||||
def generate_config_markdown_table():
|
||||
import pandas as pd
|
||||
|
||||
rows = []
|
||||
for k, default, type_ in VALIDATION_TUPLES:
|
||||
description = DOCS[k]
|
||||
|
|
@ -84,12 +82,11 @@ def _schema_to_dict(schema: vol.Schema) -> dict[str, tuple[Any, Any]]:
|
|||
|
||||
|
||||
def _generate_service_markdown_table(
|
||||
schema: dict[str, tuple[Any, Any]],
|
||||
schema: vol.Schema,
|
||||
alternative_docs: dict[str, str] | None = None,
|
||||
):
|
||||
schema = _schema_to_dict(schema)
|
||||
rows = []
|
||||
for k, (default, type_) in schema.items():
|
||||
for k, (default, type_) in _schema_to_dict(schema).items():
|
||||
if alternative_docs is not None and k in alternative_docs:
|
||||
description = alternative_docs[k]
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ def _split_service_call_data(service_data: ServiceData) -> list[ServiceData]:
|
|||
common_data = {k: service_data[k] for k in common_attrs if k in service_data}
|
||||
|
||||
attributes_split_sequence = [BRIGHTNESS_ATTRS, COLOR_ATTRS]
|
||||
service_datas = []
|
||||
service_datas: list[dict[str, Any]] = []
|
||||
|
||||
for attributes in attributes_split_sequence:
|
||||
split_data = {
|
||||
|
|
@ -106,7 +106,7 @@ async def _create_service_call_data_iterator(
|
|||
hass: HomeAssistant,
|
||||
service_datas: list[ServiceData],
|
||||
filter_by_state: bool,
|
||||
) -> AsyncGenerator[ServiceData, None]:
|
||||
) -> AsyncGenerator[ServiceData]:
|
||||
"""Enumerates and filters a list of service datas on the fly.
|
||||
|
||||
If filtering is enabled, every service data is filtered by the current state of
|
||||
|
|
@ -141,7 +141,7 @@ class AdaptationData:
|
|||
entity_id: str
|
||||
context: Context
|
||||
sleep_time: float
|
||||
service_call_datas: AsyncGenerator[ServiceData, None]
|
||||
service_call_datas: AsyncGenerator[ServiceData]
|
||||
force: bool
|
||||
max_length: int
|
||||
which: Literal["brightness", "color", "both"]
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import datetime
|
|||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import cached_property, partial
|
||||
from datetime import UTC, timedelta
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from homeassistant.util.color import (
|
||||
|
|
@ -17,9 +17,10 @@ from homeassistant.util.color import (
|
|||
color_temperature_to_rgb,
|
||||
color_xy_to_hs,
|
||||
)
|
||||
from propcache.api import cached_property
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import astral
|
||||
import astral.location
|
||||
|
||||
# Same as homeassistant.const.SUN_EVENT_SUNRISE and homeassistant.const.SUN_EVENT_SUNSET
|
||||
# We re-define them here to not depend on homeassistant in this file.
|
||||
|
|
@ -32,7 +33,6 @@ SUN_EVENT_MIDNIGHT = "solar_midnight"
|
|||
_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT)
|
||||
_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))}
|
||||
|
||||
UTC = datetime.timezone.utc
|
||||
utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC)
|
||||
utcnow.__doc__ = "Get now in UTC time."
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ class SunEvents:
|
|||
"""Track the state of the sun and associated light settings."""
|
||||
|
||||
name: str
|
||||
astral_location: astral.Location
|
||||
astral_location: astral.location.Location
|
||||
sunrise_time: datetime.time | None
|
||||
min_sunrise_time: datetime.time | None
|
||||
max_sunrise_time: datetime.time | None
|
||||
|
|
@ -198,7 +198,7 @@ class SunLightSettings:
|
|||
"""Track the state of the sun and associated light settings."""
|
||||
|
||||
name: str
|
||||
astral_location: astral.Location
|
||||
astral_location: astral.location.Location
|
||||
adapt_until_sleep: bool
|
||||
max_brightness: int
|
||||
max_color_temp: int
|
||||
|
|
@ -296,7 +296,7 @@ class SunLightSettings:
|
|||
)
|
||||
return clamp(brightness, self.min_brightness, self.max_brightness)
|
||||
|
||||
def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float:
|
||||
def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float | None:
|
||||
"""Calculate the brightness in %."""
|
||||
if is_sleep:
|
||||
return self.sleep_brightness
|
||||
|
|
@ -331,7 +331,7 @@ class SunLightSettings:
|
|||
) -> dict[str, Any]:
|
||||
"""Calculate the brightness and color."""
|
||||
sun_position = self.sun.sun_position(dt)
|
||||
rgb_color: tuple[float, float, float]
|
||||
rgb_color: tuple[int, int, int]
|
||||
# Variable `force_rgb_color` is needed for RGB color after sunset (if enabled)
|
||||
force_rgb_color = False
|
||||
brightness_pct = self.brightness_pct(dt, is_sleep)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Config flow for Adaptive Lighting integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
|
|
@ -40,7 +41,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input=None):
|
||||
async def async_step_import(self, user_input: dict[str, Any]):
|
||||
"""Handle configuration by YAML file."""
|
||||
await self.async_set_unique_id(user_input[CONF_NAME])
|
||||
# Keep a list of switches that are configured via YAML
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import logging
|
|||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import device_registry, entity_registry
|
||||
from homeassistant.util.read_only_dict import ReadOnlyDict
|
||||
|
||||
from .adaptation_utils import ServiceData
|
||||
|
|
@ -11,6 +12,25 @@ from .adaptation_utils import ServiceData
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def area_entities(hass: HomeAssistant, area_id: str):
|
||||
"""Get all entities linked to an area."""
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
entity_ids = [
|
||||
entry.entity_id
|
||||
for entry in entity_registry.async_entries_for_area(ent_reg, area_id)
|
||||
]
|
||||
dev_reg = device_registry.async_get(hass)
|
||||
entity_ids.extend(
|
||||
[
|
||||
entity.entity_id
|
||||
for device in device_registry.async_entries_for_area(dev_reg, area_id)
|
||||
for entity in entity_registry.async_entries_for_device(ent_reg, device.id)
|
||||
if entity.area_id is None
|
||||
],
|
||||
)
|
||||
return entity_ids
|
||||
|
||||
|
||||
def setup_service_call_interceptor(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@
|
|||
"iot_class": "calculated",
|
||||
"issue_tracker": "https://github.com/basnijholt/adaptive-lighting/issues",
|
||||
"requirements": ["ulid-transform"],
|
||||
"version": "1.26.0"
|
||||
"version": "1.27.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ from homeassistant.helpers.event import (
|
|||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.sun import get_astral_location
|
||||
from homeassistant.helpers.template import area_entities
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_to_rgb,
|
||||
|
|
@ -153,7 +152,7 @@ from .const import (
|
|||
apply_service_schema,
|
||||
replace_none_str,
|
||||
)
|
||||
from .hass_utils import setup_service_call_interceptor
|
||||
from .hass_utils import area_entities, setup_service_call_interceptor
|
||||
from .helpers import (
|
||||
clamp,
|
||||
color_difference_redmean,
|
||||
|
|
@ -1589,7 +1588,7 @@ class SimpleSwitch(SwitchEntity, RestoreEntity):
|
|||
self.hass = hass
|
||||
data = validate(config_entry)
|
||||
self._icon = icon
|
||||
self._state: bool | None = None
|
||||
self._state: bool = initial_state
|
||||
self._which = which
|
||||
self._config_name = data[CONF_NAME]
|
||||
self._unique_id = f"{self._config_name}_{slugify(self._which)}"
|
||||
|
|
@ -2706,7 +2705,6 @@ class _AsyncSingleShotTimer:
|
|||
|
||||
async def _run(self):
|
||||
"""Run the timer. Don't call this directly, use start() instead."""
|
||||
self.start_time = dt_util.utcnow()
|
||||
await asyncio.sleep(self.delay)
|
||||
if self.callback:
|
||||
if asyncio.iscoroutinefunction(self.callback):
|
||||
|
|
@ -2722,6 +2720,10 @@ class _AsyncSingleShotTimer:
|
|||
"""Start the timer."""
|
||||
if self.task is not None and not self.task.done():
|
||||
self.task.cancel()
|
||||
# Set start_time before creating task to avoid race condition
|
||||
# where is_running() returns True but start_time is still None
|
||||
# See: https://github.com/basnijholt/adaptive-lighting/issues/1272
|
||||
self.start_time = dt_util.utcnow()
|
||||
self.task = asyncio.create_task(self._run())
|
||||
|
||||
def cancel(self):
|
||||
|
|
|
|||
|
|
@ -6,6 +6,22 @@ if grep -q 'mypy-dev==1.14.0a3' core/requirements_test.txt; then
|
|||
# mypy-dev==1.14.0a3 seems to not be available anymore, HA 2024.12 is affected
|
||||
sed -i 's/mypy-dev==1.14.0a3/mypy-dev==1.14.0a7/' core/requirements_test.txt
|
||||
fi
|
||||
if grep -q 'mypy-dev==1.16.0a1' core/requirements_test.txt; then
|
||||
# mypy-dev==1.16.0a1 seems to not be available anymore, HA 2025.2 is affected
|
||||
sed -i 's/mypy-dev==1.16.0a1/mypy-dev==1.16.0a9/' core/requirements_test.txt
|
||||
fi
|
||||
if grep -q 'mypy-dev==1.16.0a3' core/requirements_test.txt; then
|
||||
# mypy-dev==1.16.0a3 seems to not be available anymore, HA 2025.3 is affected
|
||||
sed -i 's/mypy-dev==1.16.0a3/mypy-dev==1.16.0a9/' core/requirements_test.txt
|
||||
fi
|
||||
if grep -q 'mypy-dev==1.16.0a7' core/requirements_test.txt; then
|
||||
# mypy-dev==1.16.0a7 seems to not be available anymore, HA 2025.4 is affected
|
||||
sed -i 's/mypy-dev==1.16.0a7/mypy-dev==1.16.0a9/' core/requirements_test.txt
|
||||
fi
|
||||
if grep -q 'mypy-dev==1.16.0a8' core/requirements_test.txt; then
|
||||
# mypy-dev==1.16.0a8 seems to not be available anymore, HA 2025.5 and 2025.6 is affected
|
||||
sed -i 's/mypy-dev==1.16.0a8/mypy-dev==1.16.0a9/' core/requirements_test.txt
|
||||
fi
|
||||
|
||||
uv pip install -r core/requirements.txt
|
||||
uv pip install -r core/requirements_test.txt
|
||||
|
|
|
|||
26
tests/conftest.py
Normal file
26
tests/conftest.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""Pytest configuration for adaptive-lighting tests."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_template_deprecation_issue():
|
||||
"""Mock the template deprecation issue creation.
|
||||
|
||||
The template component's legacy platform syntax creates deprecation
|
||||
issues that require translations. Since adaptive-lighting tests use
|
||||
template lights as test fixtures (not testing the template integration
|
||||
itself), we mock the issue creation to avoid translation validation errors.
|
||||
"""
|
||||
# Patch the create_legacy_template_issue function in the template helpers
|
||||
# to be a no-op when called for the deprecated_legacy_templates issue
|
||||
try:
|
||||
with patch(
|
||||
"homeassistant.components.template.helpers.create_legacy_template_issue",
|
||||
):
|
||||
yield
|
||||
except (ImportError, ModuleNotFoundError, AttributeError):
|
||||
# Older HA versions don't have this function
|
||||
yield
|
||||
|
|
@ -65,6 +65,7 @@ from homeassistant.components.adaptive_lighting.switch import (
|
|||
CONF_INTERCEPT,
|
||||
AdaptiveLightingManager,
|
||||
AdaptiveSwitch,
|
||||
SimpleSwitch,
|
||||
_attributes_have_changed,
|
||||
color_difference_redmean,
|
||||
create_context,
|
||||
|
|
@ -82,7 +83,16 @@ from homeassistant.components.light import (
|
|||
)
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.components.template.light import LightTemplate
|
||||
|
||||
try:
|
||||
# HA >= 2025.8
|
||||
from homeassistant.components.template.light import (
|
||||
StateLightEntity as LightTemplate,
|
||||
)
|
||||
except ImportError:
|
||||
# HA < 2025.8
|
||||
from homeassistant.components.template.light import LightTemplate
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_AREA_ID,
|
||||
|
|
@ -2266,3 +2276,76 @@ async def test_brightness_mode(hass, brightness_mode, dark, light):
|
|||
# After sunrise the brightness should be light_brightness
|
||||
await patch_time_and_update(after_sunrise)
|
||||
assert is_approx_equal(switch._settings[ATTR_BRIGHTNESS_PCT], light_brightness)
|
||||
|
||||
|
||||
async def test_simple_switch_initial_state_not_none(hass):
|
||||
"""Test that SimpleSwitch._state is not None after __init__.
|
||||
|
||||
Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1264
|
||||
|
||||
When an entity is disabled in Home Assistant, async_added_to_hass() is never
|
||||
called. Previously, SimpleSwitch._state was initialized to None and only set
|
||||
to True/False in async_added_to_hass(). This caused an infinite loop in
|
||||
AdaptiveSwitch._setup_listeners() which waits for all SimpleSwitch._state
|
||||
to be not None.
|
||||
|
||||
The fix is to initialize _state to the initial_state value in __init__.
|
||||
"""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
# Create a SimpleSwitch without calling async_added_to_hass
|
||||
# (simulating a disabled entity)
|
||||
switch = SimpleSwitch(
|
||||
which="Test",
|
||||
initial_state=True,
|
||||
hass=hass,
|
||||
config_entry=entry,
|
||||
icon="mdi:test",
|
||||
)
|
||||
|
||||
# Before the fix: _state would be None, causing infinite loop
|
||||
# After the fix: _state should be the initial_state value
|
||||
assert switch._state is not None, (
|
||||
"SimpleSwitch._state should not be None after __init__. "
|
||||
"This would cause an infinite loop in _setup_listeners when the entity is disabled."
|
||||
)
|
||||
assert switch._state is True # Should be the initial_state value
|
||||
|
||||
|
||||
async def test_simple_switch_state_after_async_added_to_hass(hass):
|
||||
"""Test that SimpleSwitch._state is properly set after async_added_to_hass.
|
||||
|
||||
This ensures the fix for #1264 doesn't break normal entity initialization.
|
||||
"""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
# Create switches with different initial states
|
||||
switch_true = SimpleSwitch(
|
||||
which="Test True",
|
||||
initial_state=True,
|
||||
hass=hass,
|
||||
config_entry=entry,
|
||||
icon="mdi:test",
|
||||
)
|
||||
switch_false = SimpleSwitch(
|
||||
which="Test False",
|
||||
initial_state=False,
|
||||
hass=hass,
|
||||
config_entry=entry,
|
||||
icon="mdi:test",
|
||||
)
|
||||
|
||||
# Verify initial state is set correctly
|
||||
assert switch_true._state is True
|
||||
assert switch_false._state is False
|
||||
|
||||
# Call async_added_to_hass (simulating normal entity setup)
|
||||
# Since there's no last state, it should use the initial_state
|
||||
await switch_true.async_added_to_hass()
|
||||
await switch_false.async_added_to_hass()
|
||||
|
||||
# State should still be correct after async_added_to_hass
|
||||
assert switch_true._state is True
|
||||
assert switch_false._state is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue