diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 30c9fbe9a3a..6467869610d 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -24,6 +24,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv import homeassistant.util as util @@ -71,7 +72,7 @@ ATTR_BATTERY = 'battery' ATTR_ATTRIBUTES = 'attributes' PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_SCAN_INTERVAL): cv.positive_int, # seconds + vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, vol.Optional(CONF_CONSIDER_HOME, default=timedelta(seconds=DEFAULT_CONSIDER_HOME)): vol.All( @@ -639,8 +640,9 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, seen.add(mac) hass.async_add_job(async_see_device(mac=mac, host_name=host_name)) - async_track_utc_time_change( - hass, async_device_tracker_scan, second=range(0, 60, interval)) + async_track_time_interval( + hass, async_device_tracker_scan, + timedelta(seconds=interval)) hass.async_add_job(async_device_tracker_scan, None) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 57ab1582454..b78eedec8c2 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -405,8 +405,7 @@ def key_dependency(key, dependency): PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): string, - vol.Optional(CONF_SCAN_INTERVAL): - vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_SCAN_INTERVAL): time_period }, extra=vol.ALLOW_EXTRA) EVENT_SCHEMA = vol.Schema({ diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index cd49a5e237e..71ae352c39f 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -1,5 +1,6 @@ """Helpers for components that manage entities.""" import asyncio +from datetime import timedelta from homeassistant import config as conf_util from homeassistant.bootstrap import ( @@ -12,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import get_component from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.event import async_track_utc_time_change +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import extract_entity_ids from homeassistant.util.async import ( run_callback_threadsafe, run_coroutine_threadsafe) @@ -324,9 +325,10 @@ class EntityPlatform(object): in self.platform_entities): return - self._async_unsub_polling = async_track_utc_time_change( + self._async_unsub_polling = async_track_time_interval( self.component.hass, self._update_entity_states, - second=range(0, 60, self.scan_interval)) + timedelta(seconds=self.scan_interval) + ) @asyncio.coroutine def _async_process_entity(self, new_entity, update_before_add): diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index dd00cfee30e..29d3d131f5c 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -85,7 +85,7 @@ track_state_change = threaded_listener_factory(async_track_state_change) def async_track_point_in_time(hass, action, point_in_time): - """Add a listener that fires once after a spefic point in time.""" + """Add a listener that fires once after a specific point in time.""" utc_point_in_time = dt_util.as_utc(point_in_time) @callback @@ -133,6 +133,33 @@ track_point_in_utc_time = threaded_listener_factory( async_track_point_in_utc_time) +def async_track_time_interval(hass, action, interval): + """Add a listener that fires repetitively at every timedelta interval.""" + def next_interval(): + """Return the next interval.""" + return dt_util.utcnow() + interval + + @callback + def interval_listener(now): + """Called when when the interval has elapsed.""" + nonlocal remove + remove = async_track_point_in_utc_time( + hass, interval_listener, next_interval()) + hass.async_run_job(action, now) + + remove = async_track_point_in_utc_time( + hass, interval_listener, next_interval()) + + def remove_listener(): + """Remove interval listener.""" + remove() + + return remove_listener + + +track_time_interval = threaded_listener_factory(async_track_time_interval) + + def async_track_sunrise(hass, action, offset=None): """Add a listener that will fire a specified offset from sunrise daily.""" from homeassistant.components import sun diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 1e12d7c3ea3..69c314a8208 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -5,12 +5,15 @@ from collections import OrderedDict import logging import unittest from unittest.mock import patch, Mock +from datetime import timedelta import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.components import group from homeassistant.helpers.entity import Entity, generate_entity_id -from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity_component import ( + EntityComponent, DEFAULT_SCAN_INTERVAL) + from homeassistant.helpers import discovery import homeassistant.util.dt as dt_util @@ -106,7 +109,7 @@ class TestHelpersEntityComponent(unittest.TestCase): no_poll_ent.async_update.reset_mock() poll_ent.async_update.reset_mock() - fire_time_changed(self.hass, dt_util.utcnow().replace(second=0)) + fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) self.hass.block_till_done() assert not no_poll_ent.async_update.called @@ -123,7 +126,10 @@ class TestHelpersEntityComponent(unittest.TestCase): assert 1 == len(self.hass.states.entity_ids()) ent2.update = lambda *_: component.add_entities([ent1]) - fire_time_changed(self.hass, dt_util.utcnow().replace(second=0)) + fire_time_changed( + self.hass, dt_util.utcnow() + + timedelta(seconds=DEFAULT_SCAN_INTERVAL) + ) self.hass.block_till_done() assert 2 == len(self.hass.states.entity_ids()) @@ -311,7 +317,7 @@ class TestHelpersEntityComponent(unittest.TestCase): mock_setup.call_args[0] @patch('homeassistant.helpers.entity_component.' - 'async_track_utc_time_change') + 'async_track_time_interval') def test_set_scan_interval_via_config(self, mock_track): """Test the setting of the scan interval via configuration.""" def platform_setup(hass, config, add_devices, discovery_info=None): @@ -331,10 +337,10 @@ class TestHelpersEntityComponent(unittest.TestCase): }) assert mock_track.called - assert [0, 30] == list(mock_track.call_args[1]['second']) + assert timedelta(seconds=30) == mock_track.call_args[0][2] @patch('homeassistant.helpers.entity_component.' - 'async_track_utc_time_change') + 'async_track_time_interval') def test_set_scan_interval_via_platform(self, mock_track): """Test the setting of the scan interval via platform.""" def platform_setup(hass, config, add_devices, discovery_info=None): @@ -355,7 +361,7 @@ class TestHelpersEntityComponent(unittest.TestCase): }) assert mock_track.called - assert [0, 30] == list(mock_track.call_args[1]['second']) + assert timedelta(seconds=30) == mock_track.call_args[0][2] def test_set_entity_namespace_via_config(self): """Test setting an entity namespace.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 77518241080..05d5953d08a 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -15,6 +15,7 @@ from homeassistant.helpers.event import ( track_utc_time_change, track_time_change, track_state_change, + track_time_interval, track_sunrise, track_sunset, ) @@ -187,6 +188,34 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(5, len(wildcard_runs)) self.assertEqual(6, len(wildercard_runs)) + def test_track_time_interval(self): + """Test tracking time interval.""" + specific_runs = [] + + utc_now = dt_util.utcnow() + unsub = track_time_interval( + self.hass, lambda x: specific_runs.append(1), + timedelta(seconds=10) + ) + + self._send_time_changed(utc_now + timedelta(seconds=5)) + self.hass.block_till_done() + self.assertEqual(0, len(specific_runs)) + + self._send_time_changed(utc_now + timedelta(seconds=13)) + self.hass.block_till_done() + self.assertEqual(1, len(specific_runs)) + + self._send_time_changed(utc_now + timedelta(minutes=20)) + self.hass.block_till_done() + self.assertEqual(2, len(specific_runs)) + + unsub() + + self._send_time_changed(utc_now + timedelta(seconds=30)) + self.hass.block_till_done() + self.assertEqual(2, len(specific_runs)) + def test_track_sunrise(self): """Test track the sunrise.""" latitude = 32.87336