From 77a94ea515067361041e825d6b2e43627286b2d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Mar 2024 12:01:49 -1000 Subject: [PATCH] Speed up loading sun (#113544) * Speed up loading sun * Speed up loading sun * Speed up loading sun * adjust * tweak --- homeassistant/components/sun/__init__.py | 306 ++--------------------- homeassistant/components/sun/const.py | 15 ++ homeassistant/components/sun/entity.py | 296 ++++++++++++++++++++++ homeassistant/components/sun/sensor.py | 2 +- tests/components/sun/test_init.py | 43 ++-- tests/components/sun/test_recorder.py | 4 +- 6 files changed, 351 insertions(+), 315 deletions(-) create mode 100644 homeassistant/components/sun/entity.py diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index a4964c94009..6308594f4bd 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -2,82 +2,24 @@ from __future__ import annotations -from datetime import datetime, timedelta -import logging -from typing import Any - -from astral.location import Elevation, Location - from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - EVENT_CORE_CONFIG_UPDATE, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, - Platform, -) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, event -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.sun import ( - get_astral_location, - get_location_astral_event_next, -) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.util import dt as dt_util - -from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED - -_LOGGER = logging.getLogger(__name__) - -ENTITY_ID = "sun.sun" - -STATE_ABOVE_HORIZON = "above_horizon" -STATE_BELOW_HORIZON = "below_horizon" - -STATE_ATTR_AZIMUTH = "azimuth" -STATE_ATTR_ELEVATION = "elevation" -STATE_ATTR_RISING = "rising" -STATE_ATTR_NEXT_DAWN = "next_dawn" -STATE_ATTR_NEXT_DUSK = "next_dusk" -STATE_ATTR_NEXT_MIDNIGHT = "next_midnight" -STATE_ATTR_NEXT_NOON = "next_noon" -STATE_ATTR_NEXT_RISING = "next_rising" -STATE_ATTR_NEXT_SETTING = "next_setting" - -# The algorithm used here is somewhat complicated. It aims to cut down -# the number of sensor updates over the day. It's documented best in -# the PR for the change, see the Discussion section of: -# https://github.com/home-assistant/core/pull/23832 - - -# As documented in wikipedia: https://en.wikipedia.org/wiki/Twilight -# sun is: -# < -18° of horizon - all stars visible -PHASE_NIGHT = "night" -# 18°-12° - some stars not visible -PHASE_ASTRONOMICAL_TWILIGHT = "astronomical_twilight" -# 12°-6° - horizon visible -PHASE_NAUTICAL_TWILIGHT = "nautical_twilight" -# 6°-0° - objects visible -PHASE_TWILIGHT = "twilight" -# 0°-10° above horizon, sun low on horizon -PHASE_SMALL_DAY = "small_day" -# > 10° above horizon -PHASE_DAY = "day" - -# 4 mins is one degree of arc change of the sun on its circle. -# During the night and the middle of the day we don't update -# that much since it's not important. -_PHASE_UPDATES = { - PHASE_NIGHT: timedelta(minutes=4 * 5), - PHASE_ASTRONOMICAL_TWILIGHT: timedelta(minutes=4 * 2), - PHASE_NAUTICAL_TWILIGHT: timedelta(minutes=4 * 2), - PHASE_TWILIGHT: timedelta(minutes=4), - PHASE_SMALL_DAY: timedelta(minutes=2), - PHASE_DAY: timedelta(minutes=4), -} +# The sensor platform is pre-imported here to ensure +# it gets loaded when the base component is loaded +# as we will always load it and we do not want to have +# to wait for the import executor when its busy later +# in the startup process. +from . import sensor as sensor_pre_import # noqa: F401 +from .const import ( # noqa: F401 # noqa: F401 + DOMAIN, + STATE_ABOVE_HORIZON, + STATE_BELOW_HORIZON, +) +from .entity import Sun CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) @@ -114,221 +56,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sun.remove_listeners() hass.states.async_remove(sun.entity_id) return unload_ok - - -class Sun(Entity): - """Representation of the Sun.""" - - _unrecorded_attributes = frozenset( - { - STATE_ATTR_AZIMUTH, - STATE_ATTR_ELEVATION, - STATE_ATTR_RISING, - STATE_ATTR_NEXT_DAWN, - STATE_ATTR_NEXT_DUSK, - STATE_ATTR_NEXT_MIDNIGHT, - STATE_ATTR_NEXT_NOON, - STATE_ATTR_NEXT_RISING, - STATE_ATTR_NEXT_SETTING, - } - ) - - _attr_name = "Sun" - entity_id = ENTITY_ID - # This entity is legacy and does not have a platform. - # We can't fix this easily without breaking changes. - _no_platform_reported = True - - location: Location - elevation: Elevation - next_rising: datetime - next_setting: datetime - next_dawn: datetime - next_dusk: datetime - next_midnight: datetime - next_noon: datetime - solar_elevation: float - solar_azimuth: float - rising: bool - _next_change: datetime - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the sun.""" - self.hass = hass - self.phase: str | None = None - - # This is normally done by async_internal_added_to_hass which is not called - # for sun because sun has no platform - self._state_info = { - "unrecorded_attributes": self._Entity__combined_unrecorded_attributes # type: ignore[attr-defined] - } - - self._config_listener: CALLBACK_TYPE | None = None - self._update_events_listener: CALLBACK_TYPE | None = None - self._update_sun_position_listener: CALLBACK_TYPE | None = None - self._config_listener = self.hass.bus.async_listen( - EVENT_CORE_CONFIG_UPDATE, self.update_location - ) - self.update_location(initial=True) - - @callback - def update_location(self, _: Event | None = None, initial: bool = False) -> None: - """Update location.""" - location, elevation = get_astral_location(self.hass) - if not initial and location == self.location: - return - self.location = location - self.elevation = elevation - if self._update_events_listener: - self._update_events_listener() - self.update_events() - - @callback - def remove_listeners(self) -> None: - """Remove listeners.""" - if self._config_listener: - self._config_listener() - if self._update_events_listener: - self._update_events_listener() - if self._update_sun_position_listener: - self._update_sun_position_listener() - - @property - def state(self) -> str: - """Return the state of the sun.""" - # 0.8333 is the same value as astral uses - if self.solar_elevation > -0.833: - return STATE_ABOVE_HORIZON - - return STATE_BELOW_HORIZON - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of the sun.""" - return { - STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(), - STATE_ATTR_NEXT_DUSK: self.next_dusk.isoformat(), - STATE_ATTR_NEXT_MIDNIGHT: self.next_midnight.isoformat(), - STATE_ATTR_NEXT_NOON: self.next_noon.isoformat(), - STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(), - STATE_ATTR_NEXT_SETTING: self.next_setting.isoformat(), - STATE_ATTR_ELEVATION: self.solar_elevation, - STATE_ATTR_AZIMUTH: self.solar_azimuth, - STATE_ATTR_RISING: self.rising, - } - - def _check_event( - self, utc_point_in_time: datetime, sun_event: str, before: str | None - ) -> datetime: - next_utc = get_location_astral_event_next( - self.location, self.elevation, sun_event, utc_point_in_time - ) - if next_utc < self._next_change: - self._next_change = next_utc - self.phase = before - return next_utc - - @callback - def update_events(self, now: datetime | None = None) -> None: - """Update the attributes containing solar events.""" - # Grab current time in case system clock changed since last time we ran. - utc_point_in_time = dt_util.utcnow() - self._next_change = utc_point_in_time + timedelta(days=400) - - # Work our way around the solar cycle, figure out the next - # phase. Some of these are stored. - self.location.solar_depression = "astronomical" - self._check_event(utc_point_in_time, "dawn", PHASE_NIGHT) - self.location.solar_depression = "nautical" - self._check_event(utc_point_in_time, "dawn", PHASE_ASTRONOMICAL_TWILIGHT) - self.location.solar_depression = "civil" - self.next_dawn = self._check_event( - utc_point_in_time, "dawn", PHASE_NAUTICAL_TWILIGHT - ) - self.next_rising = self._check_event( - utc_point_in_time, SUN_EVENT_SUNRISE, PHASE_TWILIGHT - ) - self.location.solar_depression = -10 - self._check_event(utc_point_in_time, "dawn", PHASE_SMALL_DAY) - self.next_noon = self._check_event(utc_point_in_time, "noon", None) - self._check_event(utc_point_in_time, "dusk", PHASE_DAY) - self.next_setting = self._check_event( - utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY - ) - self.location.solar_depression = "civil" - self.next_dusk = self._check_event(utc_point_in_time, "dusk", PHASE_TWILIGHT) - self.location.solar_depression = "nautical" - self._check_event(utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT) - self.location.solar_depression = "astronomical" - self._check_event(utc_point_in_time, "dusk", PHASE_ASTRONOMICAL_TWILIGHT) - self.next_midnight = self._check_event(utc_point_in_time, "midnight", None) - self.location.solar_depression = "civil" - - # if the event was solar midday or midnight, phase will now - # be None. Solar noon doesn't always happen when the sun is - # even in the day at the poles, so we can't rely on it. - # Need to calculate phase if next is noon or midnight - if self.phase is None: - elevation = self.location.solar_elevation(self._next_change, self.elevation) - if elevation >= 10: - self.phase = PHASE_DAY - elif elevation >= 0: - self.phase = PHASE_SMALL_DAY - elif elevation >= -6: - self.phase = PHASE_TWILIGHT - elif elevation >= -12: - self.phase = PHASE_NAUTICAL_TWILIGHT - elif elevation >= -18: - self.phase = PHASE_ASTRONOMICAL_TWILIGHT - else: - self.phase = PHASE_NIGHT - - self.rising = self.next_noon < self.next_midnight - - _LOGGER.debug( - "sun phase_update@%s: phase=%s", utc_point_in_time.isoformat(), self.phase - ) - if self._update_sun_position_listener: - self._update_sun_position_listener() - self.update_sun_position() - async_dispatcher_send(self.hass, SIGNAL_EVENTS_CHANGED) - - # Set timer for the next solar event - self._update_events_listener = event.async_track_point_in_utc_time( - self.hass, self.update_events, self._next_change - ) - _LOGGER.debug("next time: %s", self._next_change.isoformat()) - - @callback - def update_sun_position(self, now: datetime | None = None) -> None: - """Calculate the position of the sun.""" - # Grab current time in case system clock changed since last time we ran. - utc_point_in_time = dt_util.utcnow() - self.solar_azimuth = round( - self.location.solar_azimuth(utc_point_in_time, self.elevation), 2 - ) - self.solar_elevation = round( - self.location.solar_elevation(utc_point_in_time, self.elevation), 2 - ) - - _LOGGER.debug( - "sun position_update@%s: elevation=%s azimuth=%s", - utc_point_in_time.isoformat(), - self.solar_elevation, - self.solar_azimuth, - ) - self.async_write_ha_state() - - async_dispatcher_send(self.hass, SIGNAL_POSITION_CHANGED) - - # Next update as per the current phase - assert self.phase - delta = _PHASE_UPDATES[self.phase] - # if the next update is within 1.25 of the next - # position update just drop it - if utc_point_in_time + delta * 1.25 > self._next_change: - self._update_sun_position_listener = None - return - self._update_sun_position_listener = event.async_track_point_in_utc_time( - self.hass, self.update_sun_position, utc_point_in_time + delta - ) diff --git a/homeassistant/components/sun/const.py b/homeassistant/components/sun/const.py index df7b0d43465..949bd4e2fbb 100644 --- a/homeassistant/components/sun/const.py +++ b/homeassistant/components/sun/const.py @@ -8,3 +8,18 @@ DEFAULT_NAME: Final = "Sun" SIGNAL_POSITION_CHANGED = f"{DOMAIN}_position_changed" SIGNAL_EVENTS_CHANGED = f"{DOMAIN}_events_changed" + + +STATE_ABOVE_HORIZON = "above_horizon" +STATE_BELOW_HORIZON = "below_horizon" + + +STATE_ATTR_AZIMUTH = "azimuth" +STATE_ATTR_ELEVATION = "elevation" +STATE_ATTR_RISING = "rising" +STATE_ATTR_NEXT_DAWN = "next_dawn" +STATE_ATTR_NEXT_DUSK = "next_dusk" +STATE_ATTR_NEXT_MIDNIGHT = "next_midnight" +STATE_ATTR_NEXT_NOON = "next_noon" +STATE_ATTR_NEXT_RISING = "next_rising" +STATE_ATTR_NEXT_SETTING = "next_setting" diff --git a/homeassistant/components/sun/entity.py b/homeassistant/components/sun/entity.py new file mode 100644 index 00000000000..739784697e0 --- /dev/null +++ b/homeassistant/components/sun/entity.py @@ -0,0 +1,296 @@ +"""Support for functionality to keep track of the sun.""" + +from __future__ import annotations + +from datetime import datetime, timedelta +import logging +from typing import Any + +from astral.location import Elevation, Location + +from homeassistant.const import ( + EVENT_CORE_CONFIG_UPDATE, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, +) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers import event +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.sun import ( + get_astral_location, + get_location_astral_event_next, +) +from homeassistant.util import dt as dt_util + +from .const import ( + SIGNAL_EVENTS_CHANGED, + SIGNAL_POSITION_CHANGED, + STATE_ABOVE_HORIZON, + STATE_BELOW_HORIZON, +) + +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID = "sun.sun" + +STATE_ATTR_AZIMUTH = "azimuth" +STATE_ATTR_ELEVATION = "elevation" +STATE_ATTR_RISING = "rising" +STATE_ATTR_NEXT_DAWN = "next_dawn" +STATE_ATTR_NEXT_DUSK = "next_dusk" +STATE_ATTR_NEXT_MIDNIGHT = "next_midnight" +STATE_ATTR_NEXT_NOON = "next_noon" +STATE_ATTR_NEXT_RISING = "next_rising" +STATE_ATTR_NEXT_SETTING = "next_setting" + +# The algorithm used here is somewhat complicated. It aims to cut down +# the number of sensor updates over the day. It's documented best in +# the PR for the change, see the Discussion section of: +# https://github.com/home-assistant/core/pull/23832 + + +# As documented in wikipedia: https://en.wikipedia.org/wiki/Twilight +# sun is: +# < -18° of horizon - all stars visible +PHASE_NIGHT = "night" +# 18°-12° - some stars not visible +PHASE_ASTRONOMICAL_TWILIGHT = "astronomical_twilight" +# 12°-6° - horizon visible +PHASE_NAUTICAL_TWILIGHT = "nautical_twilight" +# 6°-0° - objects visible +PHASE_TWILIGHT = "twilight" +# 0°-10° above horizon, sun low on horizon +PHASE_SMALL_DAY = "small_day" +# > 10° above horizon +PHASE_DAY = "day" + +# 4 mins is one degree of arc change of the sun on its circle. +# During the night and the middle of the day we don't update +# that much since it's not important. +_PHASE_UPDATES = { + PHASE_NIGHT: timedelta(minutes=4 * 5), + PHASE_ASTRONOMICAL_TWILIGHT: timedelta(minutes=4 * 2), + PHASE_NAUTICAL_TWILIGHT: timedelta(minutes=4 * 2), + PHASE_TWILIGHT: timedelta(minutes=4), + PHASE_SMALL_DAY: timedelta(minutes=2), + PHASE_DAY: timedelta(minutes=4), +} + + +class Sun(Entity): + """Representation of the Sun.""" + + _unrecorded_attributes = frozenset( + { + STATE_ATTR_AZIMUTH, + STATE_ATTR_ELEVATION, + STATE_ATTR_RISING, + STATE_ATTR_NEXT_DAWN, + STATE_ATTR_NEXT_DUSK, + STATE_ATTR_NEXT_MIDNIGHT, + STATE_ATTR_NEXT_NOON, + STATE_ATTR_NEXT_RISING, + STATE_ATTR_NEXT_SETTING, + } + ) + + _attr_name = "Sun" + entity_id = ENTITY_ID + # This entity is legacy and does not have a platform. + # We can't fix this easily without breaking changes. + _no_platform_reported = True + + location: Location + elevation: Elevation + next_rising: datetime + next_setting: datetime + next_dawn: datetime + next_dusk: datetime + next_midnight: datetime + next_noon: datetime + solar_elevation: float + solar_azimuth: float + rising: bool + _next_change: datetime + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the sun.""" + self.hass = hass + self.phase: str | None = None + + # This is normally done by async_internal_added_to_hass which is not called + # for sun because sun has no platform + self._state_info = { + "unrecorded_attributes": self._Entity__combined_unrecorded_attributes # type: ignore[attr-defined] + } + + self._config_listener: CALLBACK_TYPE | None = None + self._update_events_listener: CALLBACK_TYPE | None = None + self._update_sun_position_listener: CALLBACK_TYPE | None = None + self._config_listener = self.hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, self.update_location + ) + self.update_location(initial=True) + + @callback + def update_location(self, _: Event | None = None, initial: bool = False) -> None: + """Update location.""" + location, elevation = get_astral_location(self.hass) + if not initial and location == self.location: + return + self.location = location + self.elevation = elevation + if self._update_events_listener: + self._update_events_listener() + self.update_events() + + @callback + def remove_listeners(self) -> None: + """Remove listeners.""" + if self._config_listener: + self._config_listener() + if self._update_events_listener: + self._update_events_listener() + if self._update_sun_position_listener: + self._update_sun_position_listener() + + @property + def state(self) -> str: + """Return the state of the sun.""" + # 0.8333 is the same value as astral uses + if self.solar_elevation > -0.833: + return STATE_ABOVE_HORIZON + + return STATE_BELOW_HORIZON + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the sun.""" + return { + STATE_ATTR_NEXT_DAWN: self.next_dawn.isoformat(), + STATE_ATTR_NEXT_DUSK: self.next_dusk.isoformat(), + STATE_ATTR_NEXT_MIDNIGHT: self.next_midnight.isoformat(), + STATE_ATTR_NEXT_NOON: self.next_noon.isoformat(), + STATE_ATTR_NEXT_RISING: self.next_rising.isoformat(), + STATE_ATTR_NEXT_SETTING: self.next_setting.isoformat(), + STATE_ATTR_ELEVATION: self.solar_elevation, + STATE_ATTR_AZIMUTH: self.solar_azimuth, + STATE_ATTR_RISING: self.rising, + } + + def _check_event( + self, utc_point_in_time: datetime, sun_event: str, before: str | None + ) -> datetime: + next_utc = get_location_astral_event_next( + self.location, self.elevation, sun_event, utc_point_in_time + ) + if next_utc < self._next_change: + self._next_change = next_utc + self.phase = before + return next_utc + + @callback + def update_events(self, now: datetime | None = None) -> None: + """Update the attributes containing solar events.""" + # Grab current time in case system clock changed since last time we ran. + utc_point_in_time = dt_util.utcnow() + self._next_change = utc_point_in_time + timedelta(days=400) + + # Work our way around the solar cycle, figure out the next + # phase. Some of these are stored. + self.location.solar_depression = "astronomical" + self._check_event(utc_point_in_time, "dawn", PHASE_NIGHT) + self.location.solar_depression = "nautical" + self._check_event(utc_point_in_time, "dawn", PHASE_ASTRONOMICAL_TWILIGHT) + self.location.solar_depression = "civil" + self.next_dawn = self._check_event( + utc_point_in_time, "dawn", PHASE_NAUTICAL_TWILIGHT + ) + self.next_rising = self._check_event( + utc_point_in_time, SUN_EVENT_SUNRISE, PHASE_TWILIGHT + ) + self.location.solar_depression = -10 + self._check_event(utc_point_in_time, "dawn", PHASE_SMALL_DAY) + self.next_noon = self._check_event(utc_point_in_time, "noon", None) + self._check_event(utc_point_in_time, "dusk", PHASE_DAY) + self.next_setting = self._check_event( + utc_point_in_time, SUN_EVENT_SUNSET, PHASE_SMALL_DAY + ) + self.location.solar_depression = "civil" + self.next_dusk = self._check_event(utc_point_in_time, "dusk", PHASE_TWILIGHT) + self.location.solar_depression = "nautical" + self._check_event(utc_point_in_time, "dusk", PHASE_NAUTICAL_TWILIGHT) + self.location.solar_depression = "astronomical" + self._check_event(utc_point_in_time, "dusk", PHASE_ASTRONOMICAL_TWILIGHT) + self.next_midnight = self._check_event(utc_point_in_time, "midnight", None) + self.location.solar_depression = "civil" + + # if the event was solar midday or midnight, phase will now + # be None. Solar noon doesn't always happen when the sun is + # even in the day at the poles, so we can't rely on it. + # Need to calculate phase if next is noon or midnight + if self.phase is None: + elevation = self.location.solar_elevation(self._next_change, self.elevation) + if elevation >= 10: + self.phase = PHASE_DAY + elif elevation >= 0: + self.phase = PHASE_SMALL_DAY + elif elevation >= -6: + self.phase = PHASE_TWILIGHT + elif elevation >= -12: + self.phase = PHASE_NAUTICAL_TWILIGHT + elif elevation >= -18: + self.phase = PHASE_ASTRONOMICAL_TWILIGHT + else: + self.phase = PHASE_NIGHT + + self.rising = self.next_noon < self.next_midnight + + _LOGGER.debug( + "sun phase_update@%s: phase=%s", utc_point_in_time.isoformat(), self.phase + ) + if self._update_sun_position_listener: + self._update_sun_position_listener() + self.update_sun_position() + async_dispatcher_send(self.hass, SIGNAL_EVENTS_CHANGED) + + # Set timer for the next solar event + self._update_events_listener = event.async_track_point_in_utc_time( + self.hass, self.update_events, self._next_change + ) + _LOGGER.debug("next time: %s", self._next_change.isoformat()) + + @callback + def update_sun_position(self, now: datetime | None = None) -> None: + """Calculate the position of the sun.""" + # Grab current time in case system clock changed since last time we ran. + utc_point_in_time = dt_util.utcnow() + self.solar_azimuth = round( + self.location.solar_azimuth(utc_point_in_time, self.elevation), 2 + ) + self.solar_elevation = round( + self.location.solar_elevation(utc_point_in_time, self.elevation), 2 + ) + + _LOGGER.debug( + "sun position_update@%s: elevation=%s azimuth=%s", + utc_point_in_time.isoformat(), + self.solar_elevation, + self.solar_azimuth, + ) + self.async_write_ha_state() + + async_dispatcher_send(self.hass, SIGNAL_POSITION_CHANGED) + + # Next update as per the current phase + assert self.phase + delta = _PHASE_UPDATES[self.phase] + # if the next update is within 1.25 of the next + # position update just drop it + if utc_point_in_time + delta * 1.25 > self._next_change: + self._update_sun_position_listener = None + return + self._update_sun_position_listener = event.async_track_point_in_utc_time( + self.hass, self.update_sun_position, utc_point_in_time + delta + ) diff --git a/homeassistant/components/sun/sensor.py b/homeassistant/components/sun/sensor.py index 1abb1a6f23d..018ba4fa994 100644 --- a/homeassistant/components/sun/sensor.py +++ b/homeassistant/components/sun/sensor.py @@ -21,8 +21,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from . import Sun from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED +from .entity import Sun ENTITY_ID_SENSOR_FORMAT = SENSOR_DOMAIN + ".sun_{}" diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index b97596f74e8..48a214274c9 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -7,6 +7,7 @@ from freezegun import freeze_time import pytest from homeassistant.components import sun +from homeassistant.components.sun import entity from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component @@ -22,7 +23,7 @@ async def test_setting_rising(hass: HomeAssistant) -> None: await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}}) await hass.async_block_till_done() - state = hass.states.get(sun.ENTITY_ID) + state = hass.states.get(entity.ENTITY_ID) from astral import LocationInfo import astral.sun @@ -88,22 +89,22 @@ async def test_setting_rising(hass: HomeAssistant) -> None: mod += 1 assert next_dawn == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_DAWN] + state.attributes[entity.STATE_ATTR_NEXT_DAWN] ) assert next_dusk == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_DUSK] + state.attributes[entity.STATE_ATTR_NEXT_DUSK] ) assert next_midnight == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_MIDNIGHT] + state.attributes[entity.STATE_ATTR_NEXT_MIDNIGHT] ) assert next_noon == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_NOON] + state.attributes[entity.STATE_ATTR_NEXT_NOON] ) assert next_rising == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_RISING] + state.attributes[entity.STATE_ATTR_NEXT_RISING] ) assert next_setting == dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_SETTING] + state.attributes[entity.STATE_ATTR_NEXT_SETTING] ) @@ -118,29 +119,29 @@ async def test_state_change( await hass.async_block_till_done() test_time = dt_util.parse_datetime( - hass.states.get(sun.ENTITY_ID).attributes[sun.STATE_ATTR_NEXT_RISING] + hass.states.get(entity.ENTITY_ID).attributes[entity.STATE_ATTR_NEXT_RISING] ) assert test_time is not None - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_BELOW_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_BELOW_HORIZON patched_time = test_time + timedelta(seconds=5) with freeze_time(patched_time): async_fire_time_changed(hass, patched_time) await hass.async_block_till_done() - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_ABOVE_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_ABOVE_HORIZON # Update core configuration with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=now): await hass.config.async_update(longitude=hass.config.longitude + 90) await hass.async_block_till_done() - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_ABOVE_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_ABOVE_HORIZON # Test listeners are not duplicated after a core configuration change test_time = dt_util.parse_datetime( - hass.states.get(sun.ENTITY_ID).attributes[sun.STATE_ATTR_NEXT_DUSK] + hass.states.get(entity.ENTITY_ID).attributes[entity.STATE_ATTR_NEXT_DUSK] ) assert test_time is not None @@ -155,7 +156,7 @@ async def test_state_change( # Called once by time listener, once from Sun.update_events assert caplog.text.count("sun position_update") == 2 - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_BELOW_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_BELOW_HORIZON async def test_norway_in_june(hass: HomeAssistant) -> None: @@ -168,14 +169,14 @@ async def test_norway_in_june(hass: HomeAssistant) -> None: with patch("homeassistant.helpers.condition.dt_util.utcnow", return_value=june): assert await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}}) - state = hass.states.get(sun.ENTITY_ID) + state = hass.states.get(entity.ENTITY_ID) assert state is not None assert dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_RISING] + state.attributes[entity.STATE_ATTR_NEXT_RISING] ) == datetime(2016, 7, 24, 22, 59, 45, 689645, tzinfo=dt_util.UTC) assert dt_util.parse_datetime( - state.attributes[sun.STATE_ATTR_NEXT_SETTING] + state.attributes[entity.STATE_ATTR_NEXT_SETTING] ) == datetime(2016, 7, 25, 22, 17, 13, 503932, tzinfo=dt_util.UTC) assert state.state == sun.STATE_ABOVE_HORIZON @@ -223,25 +224,25 @@ async def test_setup_and_remove_config_entry(hass: HomeAssistant) -> None: await hass.async_block_till_done() # Check the platform is setup correctly - state = hass.states.get("sun.sun") + state = hass.states.get(entity.ENTITY_ID) assert state is not None test_time = dt_util.parse_datetime( - hass.states.get(sun.ENTITY_ID).attributes[sun.STATE_ATTR_NEXT_RISING] + hass.states.get(entity.ENTITY_ID).attributes[entity.STATE_ATTR_NEXT_RISING] ) assert test_time is not None - assert hass.states.get(sun.ENTITY_ID).state == sun.STATE_BELOW_HORIZON + assert hass.states.get(entity.ENTITY_ID).state == sun.STATE_BELOW_HORIZON # Remove the config entry assert await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() # Check the state is removed, and does not reappear - assert hass.states.get("sun.sun") is None + assert hass.states.get(entity.ENTITY_ID) is None patched_time = test_time + timedelta(seconds=5) with freeze_time(patched_time): async_fire_time_changed(hass, patched_time) await hass.async_block_till_done() - assert hass.states.get("sun.sun") is None + assert hass.states.get(entity.ENTITY_ID) is None diff --git a/tests/components/sun/test_recorder.py b/tests/components/sun/test_recorder.py index 3392884e20e..15c15552c40 100644 --- a/tests/components/sun/test_recorder.py +++ b/tests/components/sun/test_recorder.py @@ -6,8 +6,8 @@ from datetime import timedelta from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.history import get_significant_states -from homeassistant.components.sun import ( - DOMAIN, +from homeassistant.components.sun import DOMAIN +from homeassistant.components.sun.entity import ( STATE_ATTR_AZIMUTH, STATE_ATTR_ELEVATION, STATE_ATTR_NEXT_DAWN,