Feedreader configurable update interval and max entries (#14487)

This commit is contained in:
Malte Franken 2018-05-18 15:25:08 +10:00 committed by Sebastian Muszynski
parent 1c3293ac85
commit a3777c4ea8
2 changed files with 75 additions and 27 deletions

View File

@ -4,7 +4,7 @@ Support for RSS/Atom feeds.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/feedreader/ https://home-assistant.io/components/feedreader/
""" """
from datetime import datetime from datetime import datetime, timedelta
from logging import getLogger from logging import getLogger
from os.path import exists from os.path import exists
from threading import Lock from threading import Lock
@ -12,8 +12,8 @@ import pickle
import voluptuous as vol import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_time_interval
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['feedparser==5.2.1'] REQUIREMENTS = ['feedparser==5.2.1']
@ -21,16 +21,22 @@ REQUIREMENTS = ['feedparser==5.2.1']
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
CONF_URLS = 'urls' CONF_URLS = 'urls'
CONF_MAX_ENTRIES = 'max_entries'
DEFAULT_MAX_ENTRIES = 20
DEFAULT_SCAN_INTERVAL = timedelta(hours=1)
DOMAIN = 'feedreader' DOMAIN = 'feedreader'
EVENT_FEEDREADER = 'feedreader' EVENT_FEEDREADER = 'feedreader'
MAX_ENTRIES = 20
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: { DOMAIN: {
vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]),
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
cv.time_period,
vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES):
cv.positive_int
} }
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -38,18 +44,23 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Set up the Feedreader component.""" """Set up the Feedreader component."""
urls = config.get(DOMAIN)[CONF_URLS] urls = config.get(DOMAIN)[CONF_URLS]
scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL)
max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES)
data_file = hass.config.path("{}.pickle".format(DOMAIN)) data_file = hass.config.path("{}.pickle".format(DOMAIN))
storage = StoredData(data_file) storage = StoredData(data_file)
feeds = [FeedManager(url, hass, storage) for url in urls] feeds = [FeedManager(url, scan_interval, max_entries, hass, storage) for
url in urls]
return len(feeds) > 0 return len(feeds) > 0
class FeedManager(object): class FeedManager(object):
"""Abstraction over Feedparser module.""" """Abstraction over Feedparser module."""
def __init__(self, url, hass, storage): def __init__(self, url, scan_interval, max_entries, hass, storage):
"""Initialize the FeedManager object, poll every hour.""" """Initialize the FeedManager object, poll as per scan interval."""
self._url = url self._url = url
self._scan_interval = scan_interval
self._max_entries = max_entries
self._feed = None self._feed = None
self._hass = hass self._hass = hass
self._firstrun = True self._firstrun = True
@ -69,8 +80,8 @@ class FeedManager(object):
def _init_regular_updates(self, hass): def _init_regular_updates(self, hass):
"""Schedule regular updates at the top of the clock.""" """Schedule regular updates at the top of the clock."""
track_utc_time_change( track_time_interval(hass, lambda now: self._update(),
hass, lambda now: self._update(), minute=0, second=0) self._scan_interval)
@property @property
def last_update_successful(self): def last_update_successful(self):
@ -116,10 +127,10 @@ class FeedManager(object):
def _filter_entries(self): def _filter_entries(self):
"""Filter the entries provided and return the ones to keep.""" """Filter the entries provided and return the ones to keep."""
if len(self._feed.entries) > MAX_ENTRIES: if len(self._feed.entries) > self._max_entries:
_LOGGER.debug("Processing only the first %s entries " _LOGGER.debug("Processing only the first %s entries "
"in feed %s", MAX_ENTRIES, self._url) "in feed %s", self._max_entries, self._url)
self._feed.entries = self._feed.entries[0:MAX_ENTRIES] self._feed.entries = self._feed.entries[0:self._max_entries]
def _update_and_fire_entry(self, entry): def _update_and_fire_entry(self, entry):
"""Update last_entry_timestamp and fire entry.""" """Update last_entry_timestamp and fire entry."""

View File

@ -1,6 +1,6 @@
"""The tests for the feedreader component.""" """The tests for the feedreader component."""
import time import time
from datetime import datetime from datetime import datetime, timedelta
import unittest import unittest
from genericpath import exists from genericpath import exists
@ -11,12 +11,12 @@ from unittest.mock import patch
from homeassistant.components import feedreader from homeassistant.components import feedreader
from homeassistant.components.feedreader import CONF_URLS, FeedManager, \ from homeassistant.components.feedreader import CONF_URLS, FeedManager, \
StoredData, EVENT_FEEDREADER StoredData, EVENT_FEEDREADER, DEFAULT_SCAN_INTERVAL, CONF_MAX_ENTRIES, \
from homeassistant.const import EVENT_HOMEASSISTANT_START DEFAULT_MAX_ENTRIES
from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.setup import setup_component from homeassistant.setup import setup_component
from tests.common import get_test_home_assistant, assert_setup_component, \ from tests.common import get_test_home_assistant, load_fixture
load_fixture
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
@ -26,6 +26,18 @@ VALID_CONFIG_1 = {
CONF_URLS: [URL] CONF_URLS: [URL]
} }
} }
VALID_CONFIG_2 = {
feedreader.DOMAIN: {
CONF_URLS: [URL],
CONF_SCAN_INTERVAL: 60
}
}
VALID_CONFIG_3 = {
feedreader.DOMAIN: {
CONF_URLS: [URL],
CONF_MAX_ENTRIES: 100
}
}
class TestFeedreaderComponent(unittest.TestCase): class TestFeedreaderComponent(unittest.TestCase):
@ -45,11 +57,28 @@ class TestFeedreaderComponent(unittest.TestCase):
def test_setup_one_feed(self): def test_setup_one_feed(self):
"""Test the general setup of this component.""" """Test the general setup of this component."""
with assert_setup_component(1, 'feedreader'): with patch("homeassistant.components.feedreader."
"track_time_interval") as track_method:
self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, self.assertTrue(setup_component(self.hass, feedreader.DOMAIN,
VALID_CONFIG_1)) VALID_CONFIG_1))
track_method.assert_called_once_with(self.hass, mock.ANY,
DEFAULT_SCAN_INTERVAL)
def setup_manager(self, feed_data): def test_setup_scan_interval(self):
"""Test the setup of this component with scan interval."""
with patch("homeassistant.components.feedreader."
"track_time_interval") as track_method:
self.assertTrue(setup_component(self.hass, feedreader.DOMAIN,
VALID_CONFIG_2))
track_method.assert_called_once_with(self.hass, mock.ANY,
timedelta(seconds=60))
def test_setup_max_entries(self):
"""Test the setup of this component with max entries."""
self.assertTrue(setup_component(self.hass, feedreader.DOMAIN,
VALID_CONFIG_3))
def setup_manager(self, feed_data, max_entries=DEFAULT_MAX_ENTRIES):
"""Generic test setup method.""" """Generic test setup method."""
events = [] events = []
@ -67,12 +96,13 @@ class TestFeedreaderComponent(unittest.TestCase):
feedreader.DOMAIN)) feedreader.DOMAIN))
storage = StoredData(data_file) storage = StoredData(data_file)
with patch("homeassistant.components.feedreader." with patch("homeassistant.components.feedreader."
"track_utc_time_change") as track_method: "track_time_interval") as track_method:
manager = FeedManager(feed_data, self.hass, storage) manager = FeedManager(feed_data, DEFAULT_SCAN_INTERVAL,
max_entries, self.hass, storage)
# Can't use 'assert_called_once' here because it's not available # Can't use 'assert_called_once' here because it's not available
# in Python 3.5 yet. # in Python 3.5 yet.
track_method.assert_called_once_with(self.hass, mock.ANY, minute=0, track_method.assert_called_once_with(self.hass, mock.ANY,
second=0) DEFAULT_SCAN_INTERVAL)
# Artificially trigger update. # Artificially trigger update.
self.hass.bus.fire(EVENT_HOMEASSISTANT_START) self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
# Collect events. # Collect events.
@ -116,12 +146,18 @@ class TestFeedreaderComponent(unittest.TestCase):
manager3, events3 = self.setup_manager(feed_data3) manager3, events3 = self.setup_manager(feed_data3)
assert len(events3) == 0 assert len(events3) == 0
def test_feed_max_length(self): def test_feed_default_max_length(self):
"""Test long feed beyond the 20 entry limit.""" """Test long feed beyond the default 20 entry limit."""
feed_data = load_fixture('feedreader2.xml') feed_data = load_fixture('feedreader2.xml')
manager, events = self.setup_manager(feed_data) manager, events = self.setup_manager(feed_data)
assert len(events) == 20 assert len(events) == 20
def test_feed_max_length(self):
"""Test long feed beyond a configured 5 entry limit."""
feed_data = load_fixture('feedreader2.xml')
manager, events = self.setup_manager(feed_data, max_entries=5)
assert len(events) == 5
def test_feed_without_publication_date(self): def test_feed_without_publication_date(self):
"""Test simple feed with entry without publication date.""" """Test simple feed with entry without publication date."""
feed_data = load_fixture('feedreader3.xml') feed_data = load_fixture('feedreader3.xml')
@ -141,7 +177,8 @@ class TestFeedreaderComponent(unittest.TestCase):
data_file = self.hass.config.path("{}.pickle".format( data_file = self.hass.config.path("{}.pickle".format(
feedreader.DOMAIN)) feedreader.DOMAIN))
storage = StoredData(data_file) storage = StoredData(data_file)
manager = FeedManager("FEED DATA", self.hass, storage) manager = FeedManager("FEED DATA", DEFAULT_SCAN_INTERVAL,
DEFAULT_MAX_ENTRIES, self.hass, storage)
# Artificially trigger update. # Artificially trigger update.
self.hass.bus.fire(EVENT_HOMEASSISTANT_START) self.hass.bus.fire(EVENT_HOMEASSISTANT_START)
# Collect events. # Collect events.