diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 5bb105f8123..feb68d76f6a 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) 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, @@ -24,7 +25,7 @@ from homeassistant.helpers.sun import ( from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util -from .const import DOMAIN +from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED _LOGGER = logging.getLogger(__name__) @@ -285,6 +286,7 @@ class Sun(Entity): 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( @@ -312,6 +314,8 @@ class Sun(Entity): ) 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] diff --git a/homeassistant/components/sun/const.py b/homeassistant/components/sun/const.py index f567c77e62a..245f8ca1d58 100644 --- a/homeassistant/components/sun/const.py +++ b/homeassistant/components/sun/const.py @@ -4,3 +4,6 @@ from typing import Final DOMAIN: Final = "sun" DEFAULT_NAME: Final = "Sun" + +SIGNAL_POSITION_CHANGED = f"{DOMAIN}_position_changed" +SIGNAL_EVENTS_CHANGED = f"{DOMAIN}_events_changed" diff --git a/homeassistant/components/sun/sensor.py b/homeassistant/components/sun/sensor.py index 6eccbc93d37..f83564bbac3 100644 --- a/homeassistant/components/sun/sensor.py +++ b/homeassistant/components/sun/sensor.py @@ -16,11 +16,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEGREE, EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +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 +from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED ENTITY_ID_SENSOR_FORMAT = SENSOR_DOMAIN + ".sun_{}" @@ -30,6 +31,7 @@ class SunEntityDescriptionMixin: """Mixin for required Sun base description keys.""" value_fn: Callable[[Sun], StateType | datetime] + signal: str @dataclass @@ -44,6 +46,7 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = ( translation_key="next_dawn", icon="mdi:sun-clock", value_fn=lambda data: data.next_dawn, + signal=SIGNAL_EVENTS_CHANGED, ), SunSensorEntityDescription( key="next_dusk", @@ -51,6 +54,7 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = ( translation_key="next_dusk", icon="mdi:sun-clock", value_fn=lambda data: data.next_dusk, + signal=SIGNAL_EVENTS_CHANGED, ), SunSensorEntityDescription( key="next_midnight", @@ -58,6 +62,7 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = ( translation_key="next_midnight", icon="mdi:sun-clock", value_fn=lambda data: data.next_midnight, + signal=SIGNAL_EVENTS_CHANGED, ), SunSensorEntityDescription( key="next_noon", @@ -65,6 +70,7 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = ( translation_key="next_noon", icon="mdi:sun-clock", value_fn=lambda data: data.next_noon, + signal=SIGNAL_EVENTS_CHANGED, ), SunSensorEntityDescription( key="next_rising", @@ -72,6 +78,7 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = ( translation_key="next_rising", icon="mdi:sun-clock", value_fn=lambda data: data.next_rising, + signal=SIGNAL_EVENTS_CHANGED, ), SunSensorEntityDescription( key="next_setting", @@ -79,6 +86,7 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = ( translation_key="next_setting", icon="mdi:sun-clock", value_fn=lambda data: data.next_setting, + signal=SIGNAL_EVENTS_CHANGED, ), SunSensorEntityDescription( key="solar_elevation", @@ -88,6 +96,7 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = ( value_fn=lambda data: data.solar_elevation, entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, + signal=SIGNAL_POSITION_CHANGED, ), SunSensorEntityDescription( key="solar_azimuth", @@ -97,6 +106,7 @@ SENSOR_TYPES: tuple[SunSensorEntityDescription, ...] = ( value_fn=lambda data: data.solar_azimuth, entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, + signal=SIGNAL_POSITION_CHANGED, ), ) @@ -117,6 +127,7 @@ class SunSensor(SensorEntity): """Representation of a Sun Sensor.""" _attr_has_entity_name = True + _attr_should_poll = False _attr_entity_category = EntityCategory.DIAGNOSTIC entity_description: SunSensorEntityDescription @@ -128,7 +139,6 @@ class SunSensor(SensorEntity): self.entity_id = ENTITY_ID_SENSOR_FORMAT.format(entity_description.key) self._attr_unique_id = f"{entry_id}-{entity_description.key}" self.sun = sun - self._attr_device_info = DeviceInfo( name="Sun", identifiers={(DOMAIN, entry_id)}, @@ -138,5 +148,15 @@ class SunSensor(SensorEntity): @property def native_value(self) -> StateType | datetime: """Return value of sensor.""" - state = self.entity_description.value_fn(self.sun) - return state + return self.entity_description.value_fn(self.sun) + + async def async_added_to_hass(self) -> None: + """Register signal listener when added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self.entity_description.signal, + self.async_write_ha_state, + ) + ) diff --git a/tests/components/sun/test_sensor.py b/tests/components/sun/test_sensor.py index 38453569269..6559cc3f7e9 100644 --- a/tests/components/sun/test_sensor.py +++ b/tests/components/sun/test_sensor.py @@ -3,7 +3,8 @@ from datetime import datetime, timedelta from astral import LocationInfo import astral.sun -from freezegun import freeze_time +from freezegun.api import FrozenDateTimeFactory +import pytest from homeassistant.components import sun from homeassistant.const import EntityCategory @@ -13,12 +14,15 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -async def test_setting_rising(hass: HomeAssistant) -> None: +async def test_setting_rising( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + entity_registry_enabled_by_default: None, +) -> None: """Test retrieving sun setting and rising.""" utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - with freeze_time(utc_now): - await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}}) - + freezer.move_to(utc_now) + await async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {}}) await hass.async_block_till_done() utc_today = utc_now.date() @@ -81,6 +85,9 @@ async def test_setting_rising(hass: HomeAssistant) -> None: break mod += 1 + expected_solar_elevation = astral.sun.elevation(location.observer, utc_now) + expected_solar_azimuth = astral.sun.azimuth(location.observer, utc_now) + state1 = hass.states.get("sensor.sun_next_dawn") state2 = hass.states.get("sensor.sun_next_dusk") state3 = hass.states.get("sensor.sun_next_midnight") @@ -93,6 +100,14 @@ async def test_setting_rising(hass: HomeAssistant) -> None: assert next_noon.replace(microsecond=0) == dt_util.parse_datetime(state4.state) assert next_rising.replace(microsecond=0) == dt_util.parse_datetime(state5.state) assert next_setting.replace(microsecond=0) == dt_util.parse_datetime(state6.state) + solar_elevation_state = hass.states.get("sensor.sun_solar_elevation") + assert float(solar_elevation_state.state) == pytest.approx( + expected_solar_elevation, 0.1 + ) + solar_azimuth_state = hass.states.get("sensor.sun_solar_azimuth") + assert float(solar_azimuth_state.state) == pytest.approx( + expected_solar_azimuth, 0.1 + ) entry_ids = hass.config_entries.async_entries("sun") @@ -102,3 +117,24 @@ async def test_setting_rising(hass: HomeAssistant) -> None: assert entity assert entity.entity_category is EntityCategory.DIAGNOSTIC assert entity.unique_id == f"{entry_ids[0].entry_id}-next_dawn" + + freezer.tick(timedelta(hours=24)) + # Block once for Sun to update + await hass.async_block_till_done() + # Block another time for the sensors to update + await hass.async_block_till_done() + + # Make sure all the signals work + assert state1.state != hass.states.get("sensor.sun_next_dawn").state + assert state2.state != hass.states.get("sensor.sun_next_dusk").state + assert state3.state != hass.states.get("sensor.sun_next_midnight").state + assert state4.state != hass.states.get("sensor.sun_next_noon").state + assert state5.state != hass.states.get("sensor.sun_next_rising").state + assert state6.state != hass.states.get("sensor.sun_next_setting").state + assert ( + solar_elevation_state.state + != hass.states.get("sensor.sun_solar_elevation").state + ) + assert ( + solar_azimuth_state.state != hass.states.get("sensor.sun_solar_azimuth").state + )