Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion custom_components/feedparser/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,22 @@ def feed_url(self: FeedParserSensor) -> str:
"""Return the URL the feed is polled from."""
return self.coordinator.config.feed_url

@property
def available(self: FeedParserSensor) -> bool:
"""Stay available while a failed poll still has earlier data to show.

CoordinatorEntity goes unavailable on any failed update, and an
unavailable state carries no attributes, so a card lost the whole feed
whenever the server hiccuped. The last good feed is kept instead and
flagged with the `stale` attribute until a poll succeeds again. Only an
entity that has never had data is unavailable.
"""
return self.coordinator.data is not None

@property
def extra_state_attributes(self: FeedParserSensor) -> dict[str, Any]:
"""Return entity specific state attributes."""
return state_attributes(self.feed_url, self.channel, self.feed_entries)
attributes = state_attributes(self.feed_url, self.channel, self.feed_entries)
if not self.coordinator.last_update_success:
attributes["stale"] = True
return attributes
29 changes: 29 additions & 0 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,32 @@ def test_a_yaml_sensor_keeps_its_configured_name(feed: FeedSource) -> None:
sensor = on_a_platform(build_sensor(parsed, feed.name))
assert sensor.name == feed.name
assert written_attributes(sensor)["friendly_name"] == feed.name


def test_a_failed_poll_keeps_the_last_feed_and_marks_it_stale(
feed: FeedSource,
) -> None:
"""Test that an outage does not blank the feed a card is showing.

An unavailable state carries no attributes, so the sensor stays available
for as long as the coordinator holds data and says so with `stale`.
"""
parsed = parse_local_feed(feed, show_topn=DEFAULT_TOPN)
sensor = build_sensor(parsed, feed.name)
sensor.coordinator.last_update_success = True
assert sensor.available
assert "stale" not in sensor.extra_state_attributes

sensor.coordinator.last_update_success = False
assert sensor.available
attributes = sensor.extra_state_attributes
assert attributes["stale"] is True
assert attributes["entries"] == parsed.entries


def test_a_feed_that_never_loaded_is_unavailable(feed: FeedSource) -> None:
"""Test that without any earlier data a failed poll is still unavailable."""
sensor = build_sensor(parse_local_feed(feed, show_topn=DEFAULT_TOPN), feed.name)
sensor.coordinator.data = None # type: ignore[assignment]
sensor.coordinator.last_update_success = False
assert not sensor.available
Loading