From f7177746899761ac17d0aff93885f4f9601c73da Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:51:53 +0200 Subject: [PATCH] Don't fire a feedreader event, when there are no new entries (#178106) --- .../components/feedreader/coordinator.py | 17 +-- tests/components/feedreader/test_event.py | 113 ++++++++++++++++-- 2 files changed, 111 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/feedreader/coordinator.py b/homeassistant/components/feedreader/coordinator.py index 22c09ed4162a..d9340b0e6755 100644 --- a/homeassistant/components/feedreader/coordinator.py +++ b/homeassistant/components/feedreader/coordinator.py @@ -145,14 +145,14 @@ class FeedReaderCoordinator( assert isinstance(self._feed.entries, list) self._filter_entries() - self._publish_new_entries() + new_entries = self._publish_new_entries() _LOGGER.debug("Fetch from feed %s completed", self.url) if self._last_entry_timestamp: self._storage.async_put_timestamp(self._feed_id, self._last_entry_timestamp) - return self._feed.entries + return new_entries @callback def _filter_entries(self) -> None: @@ -185,10 +185,10 @@ class FeedReaderCoordinator( _LOGGER.debug("New event fired for entry %s", entry.get("link")) @callback - def _publish_new_entries(self) -> None: - """Publish new entries to the event bus.""" + def _publish_new_entries(self) -> list[feedparser.FeedParserDict]: + """Publish new entries to the event bus and return new entries.""" assert self._feed is not None - new_entry_count = 0 + new_entries: list[feedparser.FeedParserDict] = [] firstrun = False self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id) if not self._last_entry_timestamp: @@ -207,13 +207,14 @@ class FeedReaderCoordinator( and time_stamp > last_entry_timestamp ): self._update_and_fire_entry(entry) - new_entry_count += 1 + new_entries.append(entry) else: _LOGGER.debug("Already processed entry %s", entry.get("link")) - if new_entry_count == 0: + if not new_entries: self._log_no_entries() else: - _LOGGER.debug("%d entries published in feed %s", new_entry_count, self.url) + _LOGGER.debug("%d entries published in feed %s", len(new_entries), self.url) + return new_entries class StoredData: diff --git a/tests/components/feedreader/test_event.py b/tests/components/feedreader/test_event.py index 8f5f3870bfe2..6f3e350608e4 100644 --- a/tests/components/feedreader/test_event.py +++ b/tests/components/feedreader/test_event.py @@ -21,15 +21,69 @@ from .const import VALID_CONFIG_DEFAULT from tests.common import async_fire_time_changed +@pytest.mark.parametrize( + ("fixture_name", "expected_attributes"), + [ + ( + "feed_one_event", + { + ATTR_TITLE: "Title 1", + ATTR_LINK: "http://www.example.com/link/1", + ATTR_CONTENT: "Content 1", + ATTR_DESCRIPTION: "Description 1", + }, + ), + ( + "feed_two_event", + { + ATTR_TITLE: "Title 2", + ATTR_LINK: "http://www.example.com/link/2", + ATTR_CONTENT: "Content 2", + ATTR_DESCRIPTION: "Description 2", + }, + ), + ( + "feed_only_summary", + { + ATTR_TITLE: "Title 1", + ATTR_LINK: "http://www.example.com/link/1", + ATTR_CONTENT: "This is a summary", + ATTR_DESCRIPTION: "Description 1", + }, + ), + ], +) async def test_event_entity( - hass: HomeAssistant, feed_one_event, feed_two_event, feed_only_summary + hass: HomeAssistant, + request: pytest.FixtureRequest, + fixture_name: str, + expected_attributes: dict[str, str], ) -> None: """Test feed event entity.""" entry = create_mock_entry(VALID_CONFIG_DEFAULT) entry.add_to_hass(hass) with patch( "homeassistant.components.feedreader.coordinator.feedparser.http.get", - side_effect=[feed_one_event, feed_two_event, feed_only_summary], + side_effect=[request.getfixturevalue(fixture_name)], + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("event.mock_title") + assert state + for attribute, value in expected_attributes.items(): + assert state.attributes[attribute] == value + + +async def test_event_new_entry_sorted( + hass: HomeAssistant, feed_one_event: bytes, feed_two_event: bytes +) -> None: + """Test feed event entity fires on new event.""" + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.feedreader.coordinator.feedparser.http.get", + side_effect=[feed_one_event, feed_two_event], ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -44,7 +98,6 @@ async def test_event_entity( future = dt_util.utcnow() + timedelta(hours=1, seconds=1) async_fire_time_changed(hass, future) await hass.async_block_till_done(wait_background_tasks=True) - state = hass.states.get("event.mock_title") assert state assert state.attributes[ATTR_TITLE] == "Title 2" @@ -52,16 +105,32 @@ async def test_event_entity( assert state.attributes[ATTR_CONTENT] == "Content 2" assert state.attributes[ATTR_DESCRIPTION] == "Description 2" - future = dt_util.utcnow() + timedelta(hours=2, seconds=2) - async_fire_time_changed(hass, future) - await hass.async_block_till_done(wait_background_tasks=True) + +async def test_event_new_entry_unsorted( + hass: HomeAssistant, feed_unsorted: bytes, feed_unsorted_update: bytes +) -> None: + """Test feed event entity fires on new event.""" + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.feedreader.coordinator.feedparser.http.get", + side_effect=[feed_unsorted, feed_unsorted_update], + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("event.mock_title") assert state - assert state.attributes[ATTR_TITLE] == "Title 1" - assert state.attributes[ATTR_LINK] == "http://www.example.com/link/1" - assert state.attributes[ATTR_CONTENT] == "This is a summary" - assert state.attributes[ATTR_DESCRIPTION] == "Description 1" + assert state.attributes[ATTR_TITLE] == "Title 3" + assert state.attributes[ATTR_CONTENT] == "Content 3" + + future = dt_util.utcnow() + timedelta(hours=1, seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("event.mock_title") + assert state + assert state.attributes[ATTR_TITLE] == "Title 4" + assert state.attributes[ATTR_CONTENT] == "Content 4" @pytest.mark.parametrize( @@ -74,7 +143,7 @@ async def test_event_entity( async def test_event_htmlentities( hass: HomeAssistant, snapshot: SnapshotAssertion, - fixture_name, + fixture_name: str, request: pytest.FixtureRequest, ) -> None: """Test feed event entity with HTML Entities.""" @@ -90,3 +159,25 @@ async def test_event_htmlentities( state = hass.states.get("event.mock_title") assert state assert state.attributes == snapshot + + +async def test_event_no_new_entry(hass: HomeAssistant, feed_two_event: bytes) -> None: + """Test feed event entity is not firing when there are no new entries.""" + entry = create_mock_entry(VALID_CONFIG_DEFAULT) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.feedreader.coordinator.feedparser.http.get", + side_effect=[feed_two_event, feed_two_event], + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("event.mock_title") + assert state + old_state = state + + future = dt_util.utcnow() + timedelta(hours=1, seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done(wait_background_tasks=True) + state = hass.states.get("event.mock_title") + assert state == old_state