Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sun listener to adapt to core config updates #24464

Merged
merged 2 commits into from Jun 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
113 changes: 70 additions & 43 deletions 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
Expand Down Expand Up @@ -263,59 +266,83 @@ def remove_listener():
track_time_interval = threaded_listener_factory(async_track_time_interval)


@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
@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 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)
def async_attach(self):
"""Attach a sun listener."""
assert self._unsub_config is None

remove = async_track_point_in_utc_time(
hass, sunrise_automation_listener, get_astral_event_next(
hass, SUN_EVENT_SUNRISE, offset=offset))
self._unsub_config = self.hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, self._handle_config_event)

def remove_listener():
"""Remove sunset listener."""
remove()
self._listen_next_sun_event()

return remove_listener
@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

track_sunrise = threaded_listener_factory(async_track_sunrise)
@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_sunset(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunset daily."""
remove = None
def async_track_sunrise(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunrise daily."""
listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset)
listener.async_attach()
return listener.async_detach

@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))
track_sunrise = threaded_listener_factory(async_track_sunrise)

def remove_listener():
"""Remove sunset listener."""
remove()

return remove_listener
@callback
@bind_hass
def async_track_sunset(hass, action, offset=None):
"""Add a listener that will fire a specified offset from sunset daily."""
listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset)
listener.async_attach()
return listener.async_detach


track_sunset = threaded_listener_factory(async_track_sunset)
Expand Down
62 changes: 62 additions & 0 deletions tests/helpers/test_event.py
Expand Up @@ -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
Expand Down