mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-10 13:54:04 +02:00
fix: replace deprecated get_astral_location with get_astral_observer (#1482)
* fix: replace deprecated get_astral_location with get_astral_observer (#1481) HA 2026.7 deprecates homeassistant.helpers.sun.get_astral_location (removal planned for 2027.7) in favor of get_astral_observer, causing a deprecation warning in the HA logs. - Switch SunEvents/SunLightSettings from astral.location.Location to astral.Observer, using the astral.sun module functions (which return UTC times by default, matching the previous local=False calls). - Use get_astral_observer in switch.py, with a fallback for HA < 2026.7 that constructs the Observer directly from the HA config. - Update tests and the webapp simulator accordingly. * ci: handle removal of requirements_test_all.txt in HA 2026.8 dev HA core removed requirements_test_all.txt (home-assistant/core#171530), which made test_dependencies.py crash with FileNotFoundError and broke the dev pytest job and the Docker builds. Fall back to requirements_all.txt, which carries the same per-integration '# homeassistant.components.x' annotations. Also extend the aiohasupervisor pin lookup in scripts/setup-dependencies accordingly. * test: support modern template light config for HA 2026.6+ HA 2026.6 removed the legacy `light: platform: template` YAML format (home-assistant/core#169615), so setup_lights found no template platform on HA dev and every test using it failed with IndexError. Detect legacy support at runtime (PLATFORM_SCHEMA presence) and fall back to the modern `template:` config format. The group platform is set up before the template integration in the modern path, because setting up `template` also sets up the `light` domain, which would make a later async_setup_component(hass, LIGHT_DOMAIN, ...) a no-op.
This commit is contained in:
parent
ddaf851be3
commit
d4d3d50ada
8 changed files with 159 additions and 94 deletions
|
|
@ -11,17 +11,15 @@ from dataclasses import dataclass
|
|||
from datetime import UTC, timedelta
|
||||
from enum import Enum
|
||||
from functools import cached_property, partial
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import astral.sun
|
||||
from homeassistant.util.color import (
|
||||
color_RGB_to_xy,
|
||||
color_temperature_to_rgb,
|
||||
color_xy_to_hs,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import astral.location
|
||||
|
||||
|
||||
class SunEvent(str, Enum):
|
||||
"""A set of sun events that happen during a day."""
|
||||
|
|
@ -48,7 +46,7 @@ class SunEvents:
|
|||
"""Track the state of the sun and associated light settings."""
|
||||
|
||||
name: str
|
||||
astral_location: astral.location.Location
|
||||
astral_observer: astral.Observer
|
||||
sunrise_time: datetime.time | None
|
||||
min_sunrise_time: datetime.time | None
|
||||
max_sunrise_time: datetime.time | None
|
||||
|
|
@ -62,7 +60,7 @@ class SunEvents:
|
|||
def sunrise(self, dt: datetime.date) -> datetime.datetime:
|
||||
"""Return the (adjusted) sunrise time for the given datetime."""
|
||||
sunrise = (
|
||||
self.astral_location.sunrise(dt, local=False)
|
||||
astral.sun.sunrise(self.astral_observer, dt)
|
||||
if self.sunrise_time is None
|
||||
else self._replace_time(dt, self.sunrise_time)
|
||||
) + self.sunrise_offset
|
||||
|
|
@ -77,7 +75,7 @@ class SunEvents:
|
|||
def sunset(self, dt: datetime.date) -> datetime.datetime:
|
||||
"""Return the (adjusted) sunset time for the given datetime."""
|
||||
sunset = (
|
||||
self.astral_location.sunset(dt, local=False)
|
||||
astral.sun.sunset(self.astral_observer, dt)
|
||||
if self.sunset_time is None
|
||||
else self._replace_time(dt, self.sunset_time)
|
||||
) + self.sunset_offset
|
||||
|
|
@ -113,8 +111,8 @@ class SunEvents:
|
|||
and self.min_sunset_time is None
|
||||
and self.max_sunset_time is None
|
||||
):
|
||||
solar_noon = self.astral_location.noon(dt, local=False)
|
||||
solar_midnight = self.astral_location.midnight(dt, local=False)
|
||||
solar_noon = astral.sun.noon(self.astral_observer, dt)
|
||||
solar_midnight = astral.sun.midnight(self.astral_observer, dt)
|
||||
return solar_noon, solar_midnight
|
||||
|
||||
if sunset is None:
|
||||
|
|
@ -208,7 +206,7 @@ class SunLightSettings:
|
|||
"""Track the state of the sun and associated light settings."""
|
||||
|
||||
name: str
|
||||
astral_location: astral.location.Location
|
||||
astral_observer: astral.Observer
|
||||
adapt_until_sleep: bool
|
||||
max_brightness: int
|
||||
max_color_temp: int
|
||||
|
|
@ -236,7 +234,7 @@ class SunLightSettings:
|
|||
"""Return the SunEvents object."""
|
||||
return SunEvents(
|
||||
name=self.name,
|
||||
astral_location=self.astral_location,
|
||||
astral_observer=self.astral_observer,
|
||||
sunrise_time=self.sunrise_time,
|
||||
sunrise_offset=self.sunrise_offset,
|
||||
min_sunrise_time=self.min_sunrise_time,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ from homeassistant.helpers.event import (
|
|||
async_track_time_interval,
|
||||
)
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.sun import get_astral_location
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.color import (
|
||||
color_temperature_to_rgb,
|
||||
|
|
@ -164,6 +163,19 @@ if TYPE_CHECKING:
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import NoEventData, VolDictType
|
||||
|
||||
try:
|
||||
from homeassistant.helpers.sun import get_astral_observer
|
||||
except ImportError: # `get_astral_observer` was added in HA 2026.7
|
||||
from astral import Observer
|
||||
|
||||
def get_astral_observer(hass: HomeAssistant) -> Observer:
|
||||
"""Get an astral observer for the current HA configuration."""
|
||||
return Observer(
|
||||
hass.config.latitude,
|
||||
hass.config.longitude,
|
||||
hass.config.elevation,
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -944,11 +956,11 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity):
|
|||
)
|
||||
self._multi_light_intercept = False
|
||||
self._expand_light_groups() # updates manual control timers
|
||||
location, _ = get_astral_location(self.hass)
|
||||
observer = get_astral_observer(self.hass)
|
||||
|
||||
self._sun_light_settings = SunLightSettings(
|
||||
name=self._name,
|
||||
astral_location=location,
|
||||
astral_observer=observer,
|
||||
adapt_until_sleep=data[CONF_ADAPT_UNTIL_SLEEP],
|
||||
max_brightness=data[CONF_MAX_BRIGHTNESS],
|
||||
max_color_temp=data[CONF_MAX_COLOR_TEMP],
|
||||
|
|
|
|||
|
|
@ -12,9 +12,13 @@ uv pip install -r core/requirements_test.txt
|
|||
|
||||
# HA 2026.4+ imports aiohasupervisor from tests/components/conftest.py
|
||||
# but pins it in requirements_test_all.txt instead of requirements_test.txt.
|
||||
# HA 2026.8+ removed requirements_test_all.txt (home-assistant/core#171530);
|
||||
# the pin lives in requirements_all.txt there.
|
||||
aiohasupervisor_req=""
|
||||
if [[ -f core/requirements_test_all.txt ]]; then
|
||||
aiohasupervisor_req="$(grep -m1 '^aiohasupervisor' core/requirements_test_all.txt || true)"
|
||||
elif [[ -f core/requirements_all.txt ]]; then
|
||||
aiohasupervisor_req="$(grep -m1 '^aiohasupervisor' core/requirements_all.txt || true)"
|
||||
fi
|
||||
if [[ -n "${aiohasupervisor_req}" ]]; then
|
||||
uv pip install "${aiohasupervisor_req}"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ deps = defaultdict(list)
|
|||
components, packages = [], []
|
||||
|
||||
requirements = Path("core") / "requirements_test_all.txt"
|
||||
if not requirements.exists():
|
||||
# Removed from HA core in 2026.8 (home-assistant/core#171530); the same
|
||||
# per-integration annotations live in requirements_all.txt.
|
||||
requirements = Path("core") / "requirements_all.txt"
|
||||
|
||||
with requirements.open() as f:
|
||||
lines = f.readlines()
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from homeassistant.components.adaptive_lighting.color_and_brightness import (
|
|||
SunEvents,
|
||||
)
|
||||
|
||||
# Create a mock astral_location object
|
||||
# Create a mock astral location object (its `.observer` is passed to `SunEvents`)
|
||||
location = Location(LocationInfo())
|
||||
|
||||
LAT_LONG_TZS = [
|
||||
|
|
@ -40,7 +40,7 @@ def test_replace_time(tzinfo_and_location):
|
|||
tzinfo, location = tzinfo_and_location
|
||||
sun_events = SunEvents(
|
||||
name="test",
|
||||
astral_location=location,
|
||||
astral_observer=location.observer,
|
||||
sunrise_time=None,
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
|
|
@ -61,7 +61,7 @@ def test_sunrise_without_offset(tzinfo_and_location):
|
|||
|
||||
sun_events = SunEvents(
|
||||
name="test",
|
||||
astral_location=location,
|
||||
astral_observer=location.observer,
|
||||
sunrise_time=None,
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
|
|
@ -79,7 +79,7 @@ def test_sun_position_no_fixed_sunset_and_sunrise(tzinfo_and_location):
|
|||
tzinfo, location = tzinfo_and_location
|
||||
sun_events = SunEvents(
|
||||
name="test",
|
||||
astral_location=location,
|
||||
astral_observer=location.observer,
|
||||
sunrise_time=None,
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
|
|
@ -107,7 +107,7 @@ def test_sun_position_fixed_sunset_and_sunrise(tzinfo_and_location):
|
|||
tzinfo, location = tzinfo_and_location
|
||||
sun_events = SunEvents(
|
||||
name="test",
|
||||
astral_location=location,
|
||||
astral_observer=location.observer,
|
||||
sunrise_time=dt.time(6, 0),
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
|
|
@ -134,7 +134,7 @@ def test_noon_and_midnight(tzinfo_and_location):
|
|||
tzinfo, location = tzinfo_and_location
|
||||
sun_events = SunEvents(
|
||||
name="test",
|
||||
astral_location=location,
|
||||
astral_observer=location.observer,
|
||||
sunrise_time=None,
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
|
|
@ -153,7 +153,7 @@ def test_sun_events(tzinfo_and_location):
|
|||
tzinfo, location = tzinfo_and_location
|
||||
sun_events = SunEvents(
|
||||
name="test",
|
||||
astral_location=location,
|
||||
astral_observer=location.observer,
|
||||
sunrise_time=None,
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
|
|
@ -173,7 +173,7 @@ def test_prev_and_next_events(tzinfo_and_location):
|
|||
tzinfo, location = tzinfo_and_location
|
||||
sun_events = SunEvents(
|
||||
name="test",
|
||||
astral_location=location,
|
||||
astral_observer=location.observer,
|
||||
sunrise_time=None,
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
|
|
@ -193,7 +193,7 @@ def test_closest_event(tzinfo_and_location):
|
|||
tzinfo, location = tzinfo_and_location
|
||||
sun_events = SunEvents(
|
||||
name="test",
|
||||
astral_location=location,
|
||||
astral_observer=location.observer,
|
||||
sunrise_time=None,
|
||||
min_sunrise_time=None,
|
||||
max_sunrise_time=None,
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ except ImportError:
|
|||
# HA < 2025.8
|
||||
from homeassistant.components.template.light import LightTemplate
|
||||
|
||||
from homeassistant.components.template import light as template_light
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
ATTR_AREA_ID,
|
||||
|
|
@ -121,6 +122,10 @@ from homeassistant.util.color import color_temperature_mired_to_kelvin
|
|||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
# HA 2026.6 removed the legacy `light: platform: template` YAML format
|
||||
# (home-assistant/core#169615); use the modern `template:` format there.
|
||||
LEGACY_TEMPLATE_LIGHTS = hasattr(template_light, "PLATFORM_SCHEMA")
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUNRISE = datetime.datetime(
|
||||
|
|
@ -200,37 +205,65 @@ async def setup_switch(hass, extra_data) -> tuple[MockConfigEntry, AdaptiveSwitc
|
|||
async def setup_lights(hass: HomeAssistant, with_group: bool = False):
|
||||
"""Set up 3 light entities using the 'template' platform."""
|
||||
n = 3 if not with_group else 5 # last 2 will be put in a group
|
||||
template_lights = {
|
||||
f"light_{i}": {
|
||||
"unique_id": f"light_{i}",
|
||||
"friendly_name": f"light_{i}",
|
||||
"turn_on": None,
|
||||
"turn_off": None,
|
||||
"set_level": None,
|
||||
"set_temperature": None,
|
||||
"set_color": None,
|
||||
}
|
||||
for i in range(1, n + 1)
|
||||
|
||||
group_platform = {
|
||||
"platform": "group",
|
||||
"entities": ["light.light_4", "light.light_5"],
|
||||
"name": "Light Group",
|
||||
"unique_id": "light_group",
|
||||
"all": "false",
|
||||
}
|
||||
template_lights["light_3"]["supports_transition_template"] = True
|
||||
platforms = [{"platform": "template", "lights": template_lights}]
|
||||
|
||||
if with_group:
|
||||
platforms.append(
|
||||
{
|
||||
"platform": "group",
|
||||
"entities": ["light.light_4", "light.light_5"],
|
||||
"name": "Light Group",
|
||||
"unique_id": "light_group",
|
||||
"all": "false",
|
||||
},
|
||||
if LEGACY_TEMPLATE_LIGHTS:
|
||||
template_lights = {
|
||||
f"light_{i}": {
|
||||
"unique_id": f"light_{i}",
|
||||
"friendly_name": f"light_{i}",
|
||||
"turn_on": None,
|
||||
"turn_off": None,
|
||||
"set_level": None,
|
||||
"set_temperature": None,
|
||||
"set_color": None,
|
||||
}
|
||||
for i in range(1, n + 1)
|
||||
}
|
||||
template_lights["light_3"]["supports_transition_template"] = True
|
||||
platforms = [{"platform": "template", "lights": template_lights}]
|
||||
if with_group:
|
||||
platforms.append(group_platform)
|
||||
await async_setup_component(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
{LIGHT_DOMAIN: platforms},
|
||||
)
|
||||
else:
|
||||
if with_group:
|
||||
# Setting up `template` below also sets up the `light` domain,
|
||||
# after which `async_setup_component(hass, LIGHT_DOMAIN, ...)`
|
||||
# would be a no-op, so the group platform must be set up first.
|
||||
await async_setup_component(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
{LIGHT_DOMAIN: [group_platform]},
|
||||
)
|
||||
modern_lights = [
|
||||
{
|
||||
"name": f"light_{i}",
|
||||
"unique_id": f"light_{i}",
|
||||
"turn_on": None,
|
||||
"turn_off": None,
|
||||
"set_level": None,
|
||||
"set_temperature": None,
|
||||
"set_hs": None,
|
||||
}
|
||||
for i in range(1, n + 1)
|
||||
]
|
||||
modern_lights[2]["supports_transition"] = "{{ true }}"
|
||||
await async_setup_component(
|
||||
hass,
|
||||
"template",
|
||||
{"template": {"light": modern_lights}},
|
||||
)
|
||||
|
||||
await async_setup_component(
|
||||
hass,
|
||||
LIGHT_DOMAIN,
|
||||
{LIGHT_DOMAIN: platforms},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
if with_group:
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ from typing import Any
|
|||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import shinyswatch
|
||||
from astral import LocationInfo
|
||||
from astral.location import Location
|
||||
from astral import Observer
|
||||
from homeassistant_util_color import color_temperature_to_rgb
|
||||
from shiny import App, render, ui
|
||||
|
||||
|
|
@ -298,7 +297,6 @@ def time_to_float(time: dt.time | dt.datetime) -> float:
|
|||
|
||||
|
||||
def _kw(input):
|
||||
location = Location(LocationInfo(timezone=dt.timezone.utc))
|
||||
return {
|
||||
"name": "Adaptive Lighting Simulator",
|
||||
"adapt_until_sleep": input.adapt_until_sleep(),
|
||||
|
|
@ -324,8 +322,8 @@ def _kw(input):
|
|||
"max_sunrise_time": None,
|
||||
"min_sunset_time": None,
|
||||
"max_sunset_time": None,
|
||||
"astral_location": location,
|
||||
"timezone": location.timezone,
|
||||
"astral_observer": Observer(),
|
||||
"timezone": dt.timezone.utc,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,27 +9,30 @@ import logging
|
|||
import math
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, timedelta
|
||||
from enum import Enum
|
||||
from functools import cached_property, partial
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import astral.sun
|
||||
from homeassistant_util_color import (
|
||||
color_RGB_to_xy,
|
||||
color_temperature_to_rgb,
|
||||
color_xy_to_hs,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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.
|
||||
SUN_EVENT_SUNRISE = "sunrise"
|
||||
SUN_EVENT_SUNSET = "sunset"
|
||||
class SunEvent(str, Enum):
|
||||
"""A set of sun events that happen during a day."""
|
||||
|
||||
SUN_EVENT_NOON = "solar_noon"
|
||||
SUN_EVENT_MIDNIGHT = "solar_midnight"
|
||||
# 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.
|
||||
SUNRISE = "sunrise"
|
||||
SUNSET = "sunset"
|
||||
NOON = "solar_noon"
|
||||
MIDNIGHT = "solar_midnight"
|
||||
|
||||
_ORDER = (SUN_EVENT_SUNRISE, SUN_EVENT_NOON, SUN_EVENT_SUNSET, SUN_EVENT_MIDNIGHT)
|
||||
|
||||
_ORDER = (SunEvent.SUNRISE, SunEvent.NOON, SunEvent.SUNSET, SunEvent.MIDNIGHT)
|
||||
_ALLOWED_ORDERS = {_ORDER[i:] + _ORDER[:i] for i in range(len(_ORDER))}
|
||||
|
||||
utcnow: partial[datetime.datetime] = partial(datetime.datetime.now, UTC)
|
||||
|
|
@ -43,7 +46,7 @@ class SunEvents:
|
|||
"""Track the state of the sun and associated light settings."""
|
||||
|
||||
name: str
|
||||
astral_location: astral.location.Location
|
||||
astral_observer: astral.Observer
|
||||
sunrise_time: datetime.time | None
|
||||
min_sunrise_time: datetime.time | None
|
||||
max_sunrise_time: datetime.time | None
|
||||
|
|
@ -57,7 +60,7 @@ class SunEvents:
|
|||
def sunrise(self, dt: datetime.date) -> datetime.datetime:
|
||||
"""Return the (adjusted) sunrise time for the given datetime."""
|
||||
sunrise = (
|
||||
self.astral_location.sunrise(dt, local=False)
|
||||
astral.sun.sunrise(self.astral_observer, dt)
|
||||
if self.sunrise_time is None
|
||||
else self._replace_time(dt, self.sunrise_time)
|
||||
) + self.sunrise_offset
|
||||
|
|
@ -72,7 +75,7 @@ class SunEvents:
|
|||
def sunset(self, dt: datetime.date) -> datetime.datetime:
|
||||
"""Return the (adjusted) sunset time for the given datetime."""
|
||||
sunset = (
|
||||
self.astral_location.sunset(dt, local=False)
|
||||
astral.sun.sunset(self.astral_observer, dt)
|
||||
if self.sunset_time is None
|
||||
else self._replace_time(dt, self.sunset_time)
|
||||
) + self.sunset_offset
|
||||
|
|
@ -108,8 +111,8 @@ class SunEvents:
|
|||
and self.min_sunset_time is None
|
||||
and self.max_sunset_time is None
|
||||
):
|
||||
solar_noon = self.astral_location.noon(dt, local=False)
|
||||
solar_midnight = self.astral_location.midnight(dt, local=False)
|
||||
solar_noon = astral.sun.noon(self.astral_observer, dt)
|
||||
solar_midnight = astral.sun.midnight(self.astral_observer, dt)
|
||||
return solar_noon, solar_midnight
|
||||
|
||||
if sunset is None:
|
||||
|
|
@ -126,21 +129,21 @@ class SunEvents:
|
|||
noon = midnight + timedelta(hours=12) * (1 if midnight.hour < 12 else -1)
|
||||
return noon, midnight
|
||||
|
||||
def sun_events(self, dt: datetime.datetime) -> list[tuple[str, float]]:
|
||||
def sun_events(self, dt: datetime.datetime) -> list[tuple[SunEvent, float]]:
|
||||
"""Get the four sun event's timestamps at 'dt'."""
|
||||
sunrise = self.sunrise(dt)
|
||||
sunset = self.sunset(dt)
|
||||
solar_noon, solar_midnight = self.noon_and_midnight(dt, sunset, sunrise)
|
||||
events = [
|
||||
(SUN_EVENT_SUNRISE, sunrise.timestamp()),
|
||||
(SUN_EVENT_SUNSET, sunset.timestamp()),
|
||||
(SUN_EVENT_NOON, solar_noon.timestamp()),
|
||||
(SUN_EVENT_MIDNIGHT, solar_midnight.timestamp()),
|
||||
events: list[tuple[SunEvent, float]] = [
|
||||
(SunEvent.SUNRISE, sunrise.timestamp()),
|
||||
(SunEvent.SUNSET, sunset.timestamp()),
|
||||
(SunEvent.NOON, solar_noon.timestamp()),
|
||||
(SunEvent.MIDNIGHT, solar_midnight.timestamp()),
|
||||
]
|
||||
self._validate_sun_event_order(events)
|
||||
return events
|
||||
|
||||
def _validate_sun_event_order(self, events: list[tuple[str, float]]) -> None:
|
||||
def _validate_sun_event_order(self, events: list[tuple[SunEvent, float]]) -> None:
|
||||
"""Check if the sun events are in the expected order."""
|
||||
events = sorted(events, key=lambda x: x[1])
|
||||
events_names, _ = zip(*events, strict=True)
|
||||
|
|
@ -154,7 +157,10 @@ class SunEvents:
|
|||
_LOGGER.error(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
def prev_and_next_events(self, dt: datetime.datetime) -> list[tuple[str, float]]:
|
||||
def prev_and_next_events(
|
||||
self,
|
||||
dt: datetime.datetime,
|
||||
) -> list[tuple[SunEvent, float]]:
|
||||
"""Get the previous and next sun event."""
|
||||
events = [
|
||||
event
|
||||
|
|
@ -171,23 +177,26 @@ class SunEvents:
|
|||
(_, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt)
|
||||
h, x = (
|
||||
(prev_ts, next_ts)
|
||||
if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
|
||||
if next_event in (SunEvent.SUNSET, SunEvent.SUNRISE)
|
||||
else (next_ts, prev_ts)
|
||||
)
|
||||
# k = -1 between sunset and sunrise (sun below horizon)
|
||||
# k = 1 between sunrise and sunset (sun above horizon)
|
||||
k = 1 if next_event in (SUN_EVENT_SUNSET, SUN_EVENT_NOON) else -1
|
||||
k = 1 if next_event in (SunEvent.SUNSET, SunEvent.NOON) else -1
|
||||
return k * (1 - ((target_ts - h) / (h - x)) ** 2)
|
||||
|
||||
def closest_event(self, dt: datetime.datetime) -> tuple[str, float]:
|
||||
def closest_event(
|
||||
self,
|
||||
dt: datetime.datetime,
|
||||
) -> tuple[Literal[SunEvent.SUNRISE, SunEvent.SUNSET], float]:
|
||||
"""Get the closest sunset or sunrise event."""
|
||||
(prev_event, prev_ts), (next_event, next_ts) = self.prev_and_next_events(dt)
|
||||
if SUN_EVENT_SUNRISE in (prev_event, next_event):
|
||||
ts_event = prev_ts if prev_event == SUN_EVENT_SUNRISE else next_ts
|
||||
return SUN_EVENT_SUNRISE, ts_event
|
||||
if SUN_EVENT_SUNSET in (prev_event, next_event):
|
||||
ts_event = prev_ts if prev_event == SUN_EVENT_SUNSET else next_ts
|
||||
return SUN_EVENT_SUNSET, ts_event
|
||||
if SunEvent.SUNRISE in (prev_event, next_event):
|
||||
ts_event = prev_ts if prev_event == SunEvent.SUNRISE else next_ts
|
||||
return SunEvent.SUNRISE, ts_event
|
||||
if SunEvent.SUNSET in (prev_event, next_event):
|
||||
ts_event = prev_ts if prev_event == SunEvent.SUNSET else next_ts
|
||||
return SunEvent.SUNSET, ts_event
|
||||
msg = "No sunrise or sunset event found."
|
||||
raise ValueError(msg)
|
||||
|
||||
|
|
@ -197,7 +206,7 @@ class SunLightSettings:
|
|||
"""Track the state of the sun and associated light settings."""
|
||||
|
||||
name: str
|
||||
astral_location: astral.location.Location
|
||||
astral_observer: astral.Observer
|
||||
adapt_until_sleep: bool
|
||||
max_brightness: int
|
||||
max_color_temp: int
|
||||
|
|
@ -225,7 +234,7 @@ class SunLightSettings:
|
|||
"""Return the SunEvents object."""
|
||||
return SunEvents(
|
||||
name=self.name,
|
||||
astral_location=self.astral_location,
|
||||
astral_observer=self.astral_observer,
|
||||
sunrise_time=self.sunrise_time,
|
||||
sunrise_offset=self.sunrise_offset,
|
||||
min_sunrise_time=self.min_sunrise_time,
|
||||
|
|
@ -249,7 +258,7 @@ class SunLightSettings:
|
|||
event, ts_event = self.sun.closest_event(dt)
|
||||
dark = self.brightness_mode_time_dark.total_seconds()
|
||||
light = self.brightness_mode_time_light.total_seconds()
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
if event == SunEvent.SUNRISE:
|
||||
brightness = scaled_tanh(
|
||||
dt.timestamp() - ts_event,
|
||||
x1=-dark,
|
||||
|
|
@ -259,7 +268,7 @@ class SunLightSettings:
|
|||
y_min=self.min_brightness,
|
||||
y_max=self.max_brightness,
|
||||
)
|
||||
elif event == SUN_EVENT_SUNSET:
|
||||
elif event == SunEvent.SUNSET:
|
||||
brightness = scaled_tanh(
|
||||
dt.timestamp() - ts_event,
|
||||
x1=-light, # shifted timestamp for the start of sunset
|
||||
|
|
@ -269,6 +278,9 @@ class SunLightSettings:
|
|||
y_min=self.min_brightness,
|
||||
y_max=self.max_brightness,
|
||||
)
|
||||
else:
|
||||
msg = "Unsupported sun event"
|
||||
raise ValueError(msg)
|
||||
return clamp(brightness, self.min_brightness, self.max_brightness)
|
||||
|
||||
def _brightness_pct_linear(self, dt: datetime.datetime) -> float:
|
||||
|
|
@ -277,7 +289,7 @@ class SunLightSettings:
|
|||
# at ts_event + dt_end, brightness == end_brightness
|
||||
dark = self.brightness_mode_time_dark.total_seconds()
|
||||
light = self.brightness_mode_time_light.total_seconds()
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
if event == SunEvent.SUNRISE:
|
||||
brightness = lerp(
|
||||
dt.timestamp() - ts_event,
|
||||
x1=-dark,
|
||||
|
|
@ -285,7 +297,7 @@ class SunLightSettings:
|
|||
y1=self.min_brightness,
|
||||
y2=self.max_brightness,
|
||||
)
|
||||
elif event == SUN_EVENT_SUNSET:
|
||||
elif event == SunEvent.SUNSET:
|
||||
brightness = lerp(
|
||||
dt.timestamp() - ts_event,
|
||||
x1=-light,
|
||||
|
|
@ -293,6 +305,9 @@ class SunLightSettings:
|
|||
y1=self.max_brightness,
|
||||
y2=self.min_brightness,
|
||||
)
|
||||
else:
|
||||
msg = "Unsupported sun event"
|
||||
raise ValueError(msg)
|
||||
return clamp(brightness, self.min_brightness, self.max_brightness)
|
||||
|
||||
def brightness_pct(self, dt: datetime.datetime, is_sleep: bool) -> float | None:
|
||||
|
|
@ -356,7 +371,8 @@ class SunLightSettings:
|
|||
force_rgb_color = True
|
||||
else:
|
||||
color_temp_kelvin = self.color_temp_kelvin(sun_position)
|
||||
rgb_color = color_temperature_to_rgb(color_temp_kelvin)
|
||||
r, g, b = color_temperature_to_rgb(color_temp_kelvin)
|
||||
rgb_color = (round(r), round(g), round(b))
|
||||
# backwards compatibility for versions < 1.3.1 - see #403
|
||||
color_temp_mired: float = math.floor(1000000 / color_temp_kelvin)
|
||||
xy_color: tuple[float, float] = color_RGB_to_xy(*rgb_color)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue