Support longer-than-60-second scan_interval and interval_seconds (#5147)

* Update scan_interval and interval_seconds max to 1 day vs. 60 seconds

* Format fixes

* Add docstring on unittest.

* Added and implemented new async_track_time_interval helper.

* Format fixes, removed unused import.

* Undid whoops on unsub_polling.

* Updated unit tests for scan_interval.

* Added unit test for track_time_interval.

* Allow other forms of time interval input for scan_interval and interval_seconds
This commit is contained in:
Nick Touran 2017-01-05 14:05:16 -08:00 committed by Paulus Schoutsen
parent f88b5a9c5e
commit a36ca62445
6 changed files with 81 additions and 16 deletions

View File

@ -24,6 +24,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.util as util import homeassistant.util as util
@ -71,7 +72,7 @@ ATTR_BATTERY = 'battery'
ATTR_ATTRIBUTES = 'attributes' ATTR_ATTRIBUTES = 'attributes'
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ 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_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean,
vol.Optional(CONF_CONSIDER_HOME, vol.Optional(CONF_CONSIDER_HOME,
default=timedelta(seconds=DEFAULT_CONSIDER_HOME)): vol.All( default=timedelta(seconds=DEFAULT_CONSIDER_HOME)): vol.All(
@ -639,8 +640,9 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
seen.add(mac) seen.add(mac)
hass.async_add_job(async_see_device(mac=mac, host_name=host_name)) hass.async_add_job(async_see_device(mac=mac, host_name=host_name))
async_track_utc_time_change( async_track_time_interval(
hass, async_device_tracker_scan, second=range(0, 60, interval)) hass, async_device_tracker_scan,
timedelta(seconds=interval))
hass.async_add_job(async_device_tracker_scan, None) hass.async_add_job(async_device_tracker_scan, None)

View File

@ -405,8 +405,7 @@ def key_dependency(key, dependency):
PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): string, vol.Required(CONF_PLATFORM): string,
vol.Optional(CONF_SCAN_INTERVAL): vol.Optional(CONF_SCAN_INTERVAL): time_period
vol.All(vol.Coerce(int), vol.Range(min=1)),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
EVENT_SCHEMA = vol.Schema({ EVENT_SCHEMA = vol.Schema({

View File

@ -1,5 +1,6 @@
"""Helpers for components that manage entities.""" """Helpers for components that manage entities."""
import asyncio import asyncio
from datetime import timedelta
from homeassistant import config as conf_util from homeassistant import config as conf_util
from homeassistant.bootstrap import ( from homeassistant.bootstrap import (
@ -12,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers import config_per_platform, discovery
from homeassistant.helpers.entity import async_generate_entity_id 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.helpers.service import extract_entity_ids
from homeassistant.util.async import ( from homeassistant.util.async import (
run_callback_threadsafe, run_coroutine_threadsafe) run_callback_threadsafe, run_coroutine_threadsafe)
@ -324,9 +325,10 @@ class EntityPlatform(object):
in self.platform_entities): in self.platform_entities):
return 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, self.component.hass, self._update_entity_states,
second=range(0, 60, self.scan_interval)) timedelta(seconds=self.scan_interval)
)
@asyncio.coroutine @asyncio.coroutine
def _async_process_entity(self, new_entity, update_before_add): def _async_process_entity(self, new_entity, update_before_add):

View File

@ -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): 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) utc_point_in_time = dt_util.as_utc(point_in_time)
@callback @callback
@ -133,6 +133,33 @@ track_point_in_utc_time = threaded_listener_factory(
async_track_point_in_utc_time) 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): 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."""
from homeassistant.components import sun from homeassistant.components import sun

View File

@ -5,12 +5,15 @@ from collections import OrderedDict
import logging import logging
import unittest import unittest
from unittest.mock import patch, Mock from unittest.mock import patch, Mock
from datetime import timedelta
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.loader as loader import homeassistant.loader as loader
from homeassistant.components import group from homeassistant.components import group
from homeassistant.helpers.entity import Entity, generate_entity_id 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 from homeassistant.helpers import discovery
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -106,7 +109,7 @@ class TestHelpersEntityComponent(unittest.TestCase):
no_poll_ent.async_update.reset_mock() no_poll_ent.async_update.reset_mock()
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() self.hass.block_till_done()
assert not no_poll_ent.async_update.called assert not no_poll_ent.async_update.called
@ -123,7 +126,10 @@ class TestHelpersEntityComponent(unittest.TestCase):
assert 1 == len(self.hass.states.entity_ids()) assert 1 == len(self.hass.states.entity_ids())
ent2.update = lambda *_: component.add_entities([ent1]) 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() self.hass.block_till_done()
assert 2 == len(self.hass.states.entity_ids()) assert 2 == len(self.hass.states.entity_ids())
@ -311,7 +317,7 @@ class TestHelpersEntityComponent(unittest.TestCase):
mock_setup.call_args[0] mock_setup.call_args[0]
@patch('homeassistant.helpers.entity_component.' @patch('homeassistant.helpers.entity_component.'
'async_track_utc_time_change') 'async_track_time_interval')
def test_set_scan_interval_via_config(self, mock_track): def test_set_scan_interval_via_config(self, mock_track):
"""Test the setting of the scan interval via configuration.""" """Test the setting of the scan interval via configuration."""
def platform_setup(hass, config, add_devices, discovery_info=None): def platform_setup(hass, config, add_devices, discovery_info=None):
@ -331,10 +337,10 @@ class TestHelpersEntityComponent(unittest.TestCase):
}) })
assert mock_track.called 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.' @patch('homeassistant.helpers.entity_component.'
'async_track_utc_time_change') 'async_track_time_interval')
def test_set_scan_interval_via_platform(self, mock_track): def test_set_scan_interval_via_platform(self, mock_track):
"""Test the setting of the scan interval via platform.""" """Test the setting of the scan interval via platform."""
def platform_setup(hass, config, add_devices, discovery_info=None): def platform_setup(hass, config, add_devices, discovery_info=None):
@ -355,7 +361,7 @@ class TestHelpersEntityComponent(unittest.TestCase):
}) })
assert mock_track.called 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): def test_set_entity_namespace_via_config(self):
"""Test setting an entity namespace.""" """Test setting an entity namespace."""

View File

@ -15,6 +15,7 @@ from homeassistant.helpers.event import (
track_utc_time_change, track_utc_time_change,
track_time_change, track_time_change,
track_state_change, track_state_change,
track_time_interval,
track_sunrise, track_sunrise,
track_sunset, track_sunset,
) )
@ -187,6 +188,34 @@ class TestEventHelpers(unittest.TestCase):
self.assertEqual(5, len(wildcard_runs)) self.assertEqual(5, len(wildcard_runs))
self.assertEqual(6, len(wildercard_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): def test_track_sunrise(self):
"""Test track the sunrise.""" """Test track the sunrise."""
latitude = 32.87336 latitude = 32.87336