Sun listener to adapt to core config updates (#24464)

* Adaptable sun listener

* Lint
pull/24469/head
Paulus Schoutsen 2019-06-10 16:05:32 -07:00 committed by GitHub
parent 935240f8c3
commit 236c5deeee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 134 additions and 45 deletions

View File

@ -1,15 +1,18 @@
"""Helpers for listening to events.""" """Helpers for listening to events."""
from datetime import timedelta from datetime import timedelta
import functools as ft import functools as ft
from typing import Callable
import attr
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.sun import get_astral_event_next from homeassistant.helpers.sun import get_astral_event_next
from ..core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from ..const import ( from homeassistant.const import (
ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL,
SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET) SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, EVENT_CORE_CONFIG_UPDATE)
from ..util import dt as dt_util from homeassistant.util import dt as dt_util
from ..util.async_ import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
# PyLint does not like the use of threaded_listener_factory # PyLint does not like the use of threaded_listener_factory
# pylint: disable=invalid-name # 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) 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 @callback
@bind_hass @bind_hass
def async_track_sunrise(hass, action, offset=None): def async_track_sunrise(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunrise daily.""" """Add a listener that will fire a specified offset from sunrise daily."""
remove = None listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset)
listener.async_attach()
@callback return listener.async_detach
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
track_sunrise = threaded_listener_factory(async_track_sunrise) track_sunrise = threaded_listener_factory(async_track_sunrise)
@ -296,26 +340,9 @@ track_sunrise = threaded_listener_factory(async_track_sunrise)
@bind_hass @bind_hass
def async_track_sunset(hass, action, offset=None): def async_track_sunset(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunset daily.""" """Add a listener that will fire a specified offset from sunset daily."""
remove = None listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset)
listener.async_attach()
@callback return listener.async_detach
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
track_sunset = threaded_listener_factory(async_track_sunset) track_sunset = threaded_listener_factory(async_track_sunset)

View File

@ -436,6 +436,68 @@ async def test_track_sunrise(hass):
assert len(offset_runs) == 1 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): async def test_track_sunset(hass):
"""Test track the sunset.""" """Test track the sunset."""
latitude = 32.87336 latitude = 32.87336