mirror of
https://github.com/basnijholt/adaptive-lighting.git
synced 2026-09-27 20:34:20 +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
|
|
@ -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