Do not link nextbus coordinator to config entry (#128151)

* Do not link nextbus coordinator to config entry

* Refactor tests and add specific failure test

* Use ConfigEntryNotReady

* Cleanup coordinator
pull/128293/head
epenet 2024-10-13 14:13:37 +02:00 committed by GitHub
parent d15a9a4359
commit 7e56b595a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 206 additions and 152 deletions

View File

@ -3,6 +3,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import CONF_AGENCY, CONF_ROUTE, DOMAIN
from .coordinator import NextBusDataUpdateCoordinator
@ -27,7 +28,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator.add_stop_route(entry_stop, entry.data[CONF_ROUTE])
await coordinator.async_config_entry_first_refresh()
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise ConfigEntryNotReady from coordinator.last_exception
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@ -24,6 +24,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
super().__init__(
hass,
_LOGGER,
config_entry=None, # It is shared between multiple entries
name=DOMAIN,
update_interval=timedelta(seconds=30),
)
@ -48,13 +49,6 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
"""Check if this coordinator is tracking any routes."""
return len(self._route_stops) > 0
async def async_shutdown(self) -> None:
"""If there are no more routes, cancel any scheduled call, and ignore new runs."""
if self.has_routes():
return
await super().async_shutdown()
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from NextBus."""

View File

@ -1 +1,34 @@
"""The tests for the nexbus component."""
from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_STOP
from homeassistant.core import HomeAssistant
from .const import VALID_AGENCY_TITLE, VALID_ROUTE_TITLE, VALID_STOP_TITLE
from tests.common import MockConfigEntry
async def assert_setup_sensor(
hass: HomeAssistant,
config: dict[str, dict[str, str]],
expected_state=ConfigEntryState.LOADED,
route_title: str = VALID_ROUTE_TITLE,
) -> MockConfigEntry:
"""Set up the sensor and assert it's been created."""
unique_id = f"{config[DOMAIN][CONF_AGENCY]}_{config[DOMAIN][CONF_ROUTE]}_{config[DOMAIN][CONF_STOP]}"
config_entry = MockConfigEntry(
domain=DOMAIN,
data=config[DOMAIN],
title=f"{VALID_AGENCY_TITLE} {route_title} {VALID_STOP_TITLE}",
unique_id=unique_id,
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is expected_state
return config_entry

View File

@ -1,10 +1,13 @@
"""Test helpers for NextBus tests."""
from collections.abc import Generator
from typing import Any
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch
import pytest
from .const import BASIC_RESULTS
@pytest.fixture(
params=[
@ -128,3 +131,21 @@ def mock_nextbus_lists(
instance.route_details.side_effect = route_details_side_effect
return instance
@pytest.fixture
def mock_nextbus() -> Generator[MagicMock]:
"""Create a mock py_nextbus module."""
with patch("homeassistant.components.nextbus.coordinator.NextBusClient") as client:
yield client
@pytest.fixture
def mock_nextbus_predictions(
mock_nextbus: MagicMock,
) -> Generator[MagicMock]:
"""Create a mock of NextBusClient predictions."""
instance = mock_nextbus.return_value
instance.predictions_for_stop.return_value = BASIC_RESULTS
return instance.predictions_for_stop

View File

@ -0,0 +1,101 @@
"""Constants for NextBus tests."""
from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_STOP
VALID_AGENCY = "sfmta-cis"
VALID_ROUTE = "F"
VALID_STOP = "5184"
VALID_COORDINATOR_KEY = f"{VALID_AGENCY}-{VALID_STOP}"
VALID_AGENCY_TITLE = "San Francisco Muni"
VALID_ROUTE_TITLE = "F-Market & Wharves"
VALID_STOP_TITLE = "Market St & 7th St"
SENSOR_ID = "sensor.san_francisco_muni_f_market_wharves_market_st_7th_st"
ROUTE_2 = "G"
ROUTE_TITLE_2 = "G-Market & Wharves"
SENSOR_ID_2 = "sensor.san_francisco_muni_g_market_wharves_market_st_7th_st"
PLATFORM_CONFIG = {
SENSOR_DOMAIN: {
"platform": DOMAIN,
CONF_AGENCY: VALID_AGENCY,
CONF_ROUTE: VALID_ROUTE,
CONF_STOP: VALID_STOP,
},
}
CONFIG_BASIC = {
DOMAIN: {
CONF_AGENCY: VALID_AGENCY,
CONF_ROUTE: VALID_ROUTE,
CONF_STOP: VALID_STOP,
}
}
CONFIG_BASIC_2 = {
DOMAIN: {
CONF_AGENCY: VALID_AGENCY,
CONF_ROUTE: ROUTE_2,
CONF_STOP: VALID_STOP,
}
}
BASIC_RESULTS = [
{
"route": {
"title": VALID_ROUTE_TITLE,
"id": VALID_ROUTE,
},
"stop": {
"name": VALID_STOP_TITLE,
"id": VALID_STOP,
},
"values": [
{"minutes": 1, "timestamp": 1553807371000},
{"minutes": 2, "timestamp": 1553807372000},
{"minutes": 3, "timestamp": 1553807373000},
{"minutes": 10, "timestamp": 1553807380000},
],
},
{
"route": {
"title": ROUTE_TITLE_2,
"id": ROUTE_2,
},
"stop": {
"name": VALID_STOP_TITLE,
"id": VALID_STOP,
},
"values": [
{"minutes": 90, "timestamp": 1553807379000},
],
},
]
NO_UPCOMING = [
{
"route": {
"title": VALID_ROUTE_TITLE,
"id": VALID_ROUTE,
},
"stop": {
"name": VALID_STOP_TITLE,
"id": VALID_STOP,
},
"values": [],
},
{
"route": {
"title": ROUTE_TITLE_2,
"id": ROUTE_2,
},
"stop": {
"name": VALID_STOP_TITLE,
"id": VALID_STOP,
},
"values": [],
},
]

View File

@ -0,0 +1,27 @@
"""The tests for the nexbus sensor component."""
from unittest.mock import MagicMock
from urllib.error import HTTPError
from homeassistant.components.nextbus.coordinator import NextBusHTTPError
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from . import assert_setup_sensor
from .const import CONFIG_BASIC
async def test_setup_retry(
hass: HomeAssistant,
mock_nextbus: MagicMock,
mock_nextbus_lists: MagicMock,
mock_nextbus_predictions: MagicMock,
) -> None:
"""Verify that a list of messages are rendered correctly."""
mock_nextbus_predictions.side_effect = NextBusHTTPError(
"failed", HTTPError("url", 500, "error", MagicMock(), None)
)
await assert_setup_sensor(
hass, CONFIG_BASIC, expected_state=ConfigEntryState.SETUP_RETRY
)

View File

@ -1,161 +1,36 @@
"""The tests for the nexbus sensor component."""
from collections.abc import Generator
from copy import deepcopy
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock
from urllib.error import HTTPError
from freezegun.api import FrozenDateTimeFactory
from py_nextbus.client import NextBusFormatError, NextBusHTTPError
import pytest
from homeassistant.components import sensor
from homeassistant.components.nextbus.const import CONF_AGENCY, CONF_ROUTE, DOMAIN
from homeassistant.components.nextbus.const import DOMAIN
from homeassistant.components.nextbus.coordinator import NextBusDataUpdateCoordinator
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_NAME, CONF_STOP
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import UpdateFailed
from tests.common import MockConfigEntry, async_fire_time_changed
from . import assert_setup_sensor
from .const import (
BASIC_RESULTS,
CONFIG_BASIC,
CONFIG_BASIC_2,
NO_UPCOMING,
ROUTE_TITLE_2,
SENSOR_ID,
SENSOR_ID_2,
VALID_AGENCY,
VALID_COORDINATOR_KEY,
VALID_ROUTE_TITLE,
VALID_STOP_TITLE,
)
VALID_AGENCY = "sfmta-cis"
VALID_ROUTE = "F"
VALID_STOP = "5184"
VALID_COORDINATOR_KEY = f"{VALID_AGENCY}-{VALID_STOP}"
VALID_AGENCY_TITLE = "San Francisco Muni"
VALID_ROUTE_TITLE = "F-Market & Wharves"
VALID_STOP_TITLE = "Market St & 7th St"
SENSOR_ID = "sensor.san_francisco_muni_f_market_wharves_market_st_7th_st"
ROUTE_2 = "G"
ROUTE_TITLE_2 = "G-Market & Wharves"
SENSOR_ID_2 = "sensor.san_francisco_muni_g_market_wharves_market_st_7th_st"
PLATFORM_CONFIG = {
sensor.DOMAIN: {
"platform": DOMAIN,
CONF_AGENCY: VALID_AGENCY,
CONF_ROUTE: VALID_ROUTE,
CONF_STOP: VALID_STOP,
},
}
CONFIG_BASIC = {
DOMAIN: {
CONF_AGENCY: VALID_AGENCY,
CONF_ROUTE: VALID_ROUTE,
CONF_STOP: VALID_STOP,
}
}
CONFIG_BASIC_2 = {
DOMAIN: {
CONF_AGENCY: VALID_AGENCY,
CONF_ROUTE: ROUTE_2,
CONF_STOP: VALID_STOP,
}
}
BASIC_RESULTS = [
{
"route": {
"title": VALID_ROUTE_TITLE,
"id": VALID_ROUTE,
},
"stop": {
"name": VALID_STOP_TITLE,
"id": VALID_STOP,
},
"values": [
{"minutes": 1, "timestamp": 1553807371000},
{"minutes": 2, "timestamp": 1553807372000},
{"minutes": 3, "timestamp": 1553807373000},
{"minutes": 10, "timestamp": 1553807380000},
],
},
{
"route": {
"title": ROUTE_TITLE_2,
"id": ROUTE_2,
},
"stop": {
"name": VALID_STOP_TITLE,
"id": VALID_STOP,
},
"values": [
{"minutes": 90, "timestamp": 1553807379000},
],
},
]
NO_UPCOMING = [
{
"route": {
"title": VALID_ROUTE_TITLE,
"id": VALID_ROUTE,
},
"stop": {
"name": VALID_STOP_TITLE,
"id": VALID_STOP,
},
"values": [],
},
{
"route": {
"title": ROUTE_TITLE_2,
"id": ROUTE_2,
},
"stop": {
"name": VALID_STOP_TITLE,
"id": VALID_STOP,
},
"values": [],
},
]
@pytest.fixture
def mock_nextbus() -> Generator[MagicMock]:
"""Create a mock py_nextbus module."""
with patch("homeassistant.components.nextbus.coordinator.NextBusClient") as client:
yield client
@pytest.fixture
def mock_nextbus_predictions(
mock_nextbus: MagicMock,
) -> Generator[MagicMock]:
"""Create a mock of NextBusClient predictions."""
instance = mock_nextbus.return_value
instance.predictions_for_stop.return_value = BASIC_RESULTS
return instance.predictions_for_stop
async def assert_setup_sensor(
hass: HomeAssistant,
config: dict[str, dict[str, str]],
expected_state=ConfigEntryState.LOADED,
route_title: str = VALID_ROUTE_TITLE,
) -> MockConfigEntry:
"""Set up the sensor and assert it's been created."""
unique_id = f"{config[DOMAIN][CONF_AGENCY]}_{config[DOMAIN][CONF_ROUTE]}_{config[DOMAIN][CONF_STOP]}"
config_entry = MockConfigEntry(
domain=DOMAIN,
data=config[DOMAIN],
title=f"{VALID_AGENCY_TITLE} {route_title} {VALID_STOP_TITLE}",
unique_id=unique_id,
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is expected_state
return config_entry
from tests.common import async_fire_time_changed
async def test_predictions(