diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 5e262a47565..009c2b1e898 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,15 +1,18 @@ """Helpers for listening to events.""" from datetime import timedelta import functools as ft +from typing import Callable + +import attr from homeassistant.loader import bind_hass from homeassistant.helpers.sun import get_astral_event_next -from ..core import HomeAssistant, callback -from ..const import ( +from homeassistant.core import HomeAssistant, callback +from homeassistant.const import ( ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, - SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) -from ..util import dt as dt_util -from ..util.async_ import run_callback_threadsafe + SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, EVENT_CORE_CONFIG_UPDATE) +from homeassistant.util import dt as dt_util +from homeassistant.util.async_ import run_callback_threadsafe # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name @@ -263,30 +266,71 @@ def async_track_time_interval(hass, action, interval): track_time_interval = threaded_listener_factory(async_track_time_interval) +@attr.s +class SunListener: + """Helper class to help listen to sun events.""" + + hass = attr.ib(type=HomeAssistant) + action = attr.ib(type=Callable) + event = attr.ib(type=str) + offset = attr.ib(type=timedelta) + _unsub_sun = attr.ib(default=None) + _unsub_config = attr.ib(default=None) + + @callback + def async_attach(self): + """Attach a sun listener.""" + assert self._unsub_config is None + + self._unsub_config = self.hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, self._handle_config_event) + + self._listen_next_sun_event() + + @callback + def async_detach(self): + """Detach the sun listener.""" + assert self._unsub_sun is not None + assert self._unsub_config is not None + + self._unsub_sun() + self._unsub_sun = None + self._unsub_config() + self._unsub_config = None + + @callback + def _listen_next_sun_event(self): + """Set up the sun event listener.""" + assert self._unsub_sun is None + + self._unsub_sun = async_track_point_in_utc_time( + self.hass, self._handle_sun_event, + get_astral_event_next(self.hass, self.event, offset=self.offset) + ) + + @callback + def _handle_sun_event(self, _now): + """Handle solar event.""" + self._unsub_sun = None + self._listen_next_sun_event() + self.hass.async_run_job(self.action) + + @callback + def _handle_config_event(self, _event): + """Handle core config update.""" + assert self._unsub_sun is not None + self._unsub_sun() + self._unsub_sun = None + self._listen_next_sun_event() + + @callback @bind_hass def async_track_sunrise(hass, action, offset=None): """Add a listener that will fire a specified offset from sunrise daily.""" - remove = None - - @callback - def sunrise_automation_listener(now): - """Handle points in time to execute actions.""" - nonlocal remove - remove = async_track_point_in_utc_time( - hass, sunrise_automation_listener, get_astral_event_next( - hass, SUN_EVENT_SUNRISE, offset=offset)) - hass.async_run_job(action) - - remove = async_track_point_in_utc_time( - hass, sunrise_automation_listener, get_astral_event_next( - hass, SUN_EVENT_SUNRISE, offset=offset)) - - def remove_listener(): - """Remove sunset listener.""" - remove() - - return remove_listener + listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset) + listener.async_attach() + return listener.async_detach track_sunrise = threaded_listener_factory(async_track_sunrise) @@ -296,26 +340,9 @@ track_sunrise = threaded_listener_factory(async_track_sunrise) @bind_hass def async_track_sunset(hass, action, offset=None): """Add a listener that will fire a specified offset from sunset daily.""" - remove = None - - @callback - def sunset_automation_listener(now): - """Handle points in time to execute actions.""" - nonlocal remove - remove = async_track_point_in_utc_time( - hass, sunset_automation_listener, get_astral_event_next( - hass, SUN_EVENT_SUNSET, offset=offset)) - hass.async_run_job(action) - - remove = async_track_point_in_utc_time( - hass, sunset_automation_listener, get_astral_event_next( - hass, SUN_EVENT_SUNSET, offset=offset)) - - def remove_listener(): - """Remove sunset listener.""" - remove() - - return remove_listener + listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset) + listener.async_attach() + return listener.async_detach track_sunset = threaded_listener_factory(async_track_sunset) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 0756bab2eec..55900b7c80a 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -436,6 +436,68 @@ async def test_track_sunrise(hass): assert len(offset_runs) == 1 +async def test_track_sunrise_update_location(hass): + """Test track the sunrise.""" + # Setup sun component + hass.config.latitude = 32.87336 + hass.config.longitude = 117.22743 + assert await async_setup_component(hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # Get next sunrise + astral = Astral() + utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) + utc_today = utc_now.date() + + mod = -1 + while True: + next_rising = (astral.sunrise_utc( + utc_today + timedelta(days=mod), + hass.config.latitude, hass.config.longitude)) + if next_rising > utc_now: + break + mod += 1 + + # Track sunrise + runs = [] + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + async_track_sunrise(hass, lambda: runs.append(1)) + + # Mimick sunrise + _send_time_changed(hass, next_rising) + await hass.async_block_till_done() + assert len(runs) == 1 + + # Move! + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + await hass.config.async_update( + latitude=40.755931, + longitude=-73.984606, + ) + await hass.async_block_till_done() + + # Mimick sunrise + _send_time_changed(hass, next_rising) + await hass.async_block_till_done() + # Did not increase + assert len(runs) == 1 + + # Get next sunrise + mod = -1 + while True: + next_rising = (astral.sunrise_utc( + utc_today + timedelta(days=mod), + hass.config.latitude, hass.config.longitude)) + if next_rising > utc_now: + break + mod += 1 + + # Mimick sunrise at new location + _send_time_changed(hass, next_rising) + await hass.async_block_till_done() + assert len(runs) == 2 + + async def test_track_sunset(hass): """Test track the sunset.""" latitude = 32.87336