mirror of
https://github.com/home-assistant/core.git
synced 2025-12-24 21:06:19 +00:00
Adds ConfigFlow for London Underground (#152050)
Co-authored-by: Norbert Rittel <norbert@rittel.de> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -1 +1,36 @@
|
||||
"""The london_underground component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN as DOMAIN
|
||||
from .coordinator import LondonTubeCoordinator, LondonUndergroundConfigEntry, TubeData
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: LondonUndergroundConfigEntry
|
||||
) -> bool:
|
||||
"""Set up London Underground from a config entry."""
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
data = TubeData(session)
|
||||
coordinator = LondonTubeCoordinator(hass, data, config_entry=entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
# Forward the setup to the sensor platform
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: LondonUndergroundConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
152
homeassistant/components/london_underground/config_flow.py
Normal file
152
homeassistant/components/london_underground/config_flow.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Config flow for London Underground integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from london_tube_status import TubeData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlowWithReload,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_LINE, DEFAULT_LINES, DOMAIN, TUBE_LINES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LondonUndergroundConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for London Underground."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
_: ConfigEntry,
|
||||
) -> LondonUndergroundOptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return LondonUndergroundOptionsFlow()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
data = TubeData(session)
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await data.update()
|
||||
except TimeoutError:
|
||||
errors["base"] = "timeout_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error")
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title="London Underground",
|
||||
data={},
|
||||
options={CONF_LINE: user_input.get(CONF_LINE, DEFAULT_LINES)},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_LINE,
|
||||
default=DEFAULT_LINES,
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=TUBE_LINES,
|
||||
multiple=True,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: ConfigType) -> ConfigFlowResult:
|
||||
"""Handle import from configuration.yaml."""
|
||||
session = async_get_clientsession(self.hass)
|
||||
data = TubeData(session)
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
await data.update()
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Unexpected error trying to connect before importing config, aborting import "
|
||||
)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
_LOGGER.warning(
|
||||
"Importing London Underground config from configuration.yaml: %s",
|
||||
import_data,
|
||||
)
|
||||
# Extract lines from the sensor platform config
|
||||
lines = import_data.get(CONF_LINE, DEFAULT_LINES)
|
||||
if "London Overground" in lines:
|
||||
_LOGGER.warning(
|
||||
"London Overground was removed from the configuration as the line has been divided and renamed"
|
||||
)
|
||||
lines.remove("London Overground")
|
||||
return self.async_create_entry(
|
||||
title="London Underground",
|
||||
data={},
|
||||
options={CONF_LINE: import_data.get(CONF_LINE, DEFAULT_LINES)},
|
||||
)
|
||||
|
||||
|
||||
class LondonUndergroundOptionsFlow(OptionsFlowWithReload):
|
||||
"""Handle options."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
_LOGGER.debug(
|
||||
"Updating london underground with options flow user_input: %s",
|
||||
user_input,
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title="",
|
||||
data={CONF_LINE: user_input[CONF_LINE]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_LINE,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_LINE,
|
||||
self.config_entry.data.get(CONF_LINE, DEFAULT_LINES),
|
||||
),
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=TUBE_LINES,
|
||||
multiple=True,
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -6,7 +6,6 @@ DOMAIN = "london_underground"
|
||||
|
||||
CONF_LINE = "line"
|
||||
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
TUBE_LINES = [
|
||||
@@ -18,7 +17,7 @@ TUBE_LINES = [
|
||||
"Elizabeth line",
|
||||
"Hammersmith & City",
|
||||
"Jubilee",
|
||||
"London Overground",
|
||||
"London Overground", # no longer supported
|
||||
"Metropolitan",
|
||||
"Northern",
|
||||
"Piccadilly",
|
||||
@@ -31,3 +30,20 @@ TUBE_LINES = [
|
||||
"Weaver",
|
||||
"Windrush",
|
||||
]
|
||||
|
||||
# Default lines to monitor if none selected
|
||||
DEFAULT_LINES = [
|
||||
"Bakerloo",
|
||||
"Central",
|
||||
"Circle",
|
||||
"District",
|
||||
"DLR",
|
||||
"Elizabeth line",
|
||||
"Hammersmith & City",
|
||||
"Jubilee",
|
||||
"Metropolitan",
|
||||
"Northern",
|
||||
"Piccadilly",
|
||||
"Victoria",
|
||||
"Waterloo & City",
|
||||
]
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import cast
|
||||
|
||||
from london_tube_status import TubeData
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
@@ -15,16 +16,23 @@ from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type LondonUndergroundConfigEntry = ConfigEntry[LondonTubeCoordinator]
|
||||
|
||||
|
||||
class LondonTubeCoordinator(DataUpdateCoordinator[dict[str, dict[str, str]]]):
|
||||
"""London Underground sensor coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, data: TubeData) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
data: TubeData,
|
||||
config_entry: LondonUndergroundConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=None,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
"domain": "london_underground",
|
||||
"name": "London Underground",
|
||||
"codeowners": ["@jpbede"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/london_underground",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["london_tube_status"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["london-tube-status==0.5"]
|
||||
"requirements": ["london-tube-status==0.5"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -5,23 +5,26 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from london_tube_status import TubeData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import CONF_LINE, TUBE_LINES
|
||||
from .coordinator import LondonTubeCoordinator
|
||||
from .const import CONF_LINE, DOMAIN, TUBE_LINES
|
||||
from .coordinator import LondonTubeCoordinator, LondonUndergroundConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,18 +41,54 @@ async def async_setup_platform(
|
||||
) -> None:
|
||||
"""Set up the Tube sensor."""
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
# If configuration.yaml config exists, trigger the import flow.
|
||||
# If the config entry already exists, this will not be triggered as only one config is allowed.
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
if (
|
||||
result.get("type") is FlowResultType.ABORT
|
||||
and result.get("reason") != "already_configured"
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{result.get('reason')}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml_import_issue",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "London Underground",
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
data = TubeData(session)
|
||||
coordinator = LondonTubeCoordinator(hass, data)
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
"deprecated_yaml",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "London Underground",
|
||||
},
|
||||
)
|
||||
|
||||
await coordinator.async_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise PlatformNotReady
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LondonUndergroundConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the London Underground sensor from config entry."""
|
||||
|
||||
async_add_entities(
|
||||
LondonTubeSensor(coordinator, line) for line in config[CONF_LINE]
|
||||
LondonTubeSensor(entry.runtime_data, line) for line in entry.options[CONF_LINE]
|
||||
)
|
||||
|
||||
|
||||
@@ -58,11 +97,21 @@ class LondonTubeSensor(CoordinatorEntity[LondonTubeCoordinator], SensorEntity):
|
||||
|
||||
_attr_attribution = "Powered by TfL Open Data"
|
||||
_attr_icon = "mdi:subway"
|
||||
_attr_has_entity_name = True # Use modern entity naming
|
||||
|
||||
def __init__(self, coordinator: LondonTubeCoordinator, name: str) -> None:
|
||||
"""Initialize the London Underground sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._name = name
|
||||
# Add unique_id for proper entity registry
|
||||
self._attr_unique_id = f"tube_{name.lower().replace(' ', '_')}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, DOMAIN)},
|
||||
name="London Underground",
|
||||
manufacturer="Transport for London",
|
||||
model="Tube Status",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
38
homeassistant/components/london_underground/strings.json
Normal file
38
homeassistant/components/london_underground/strings.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up London Underground",
|
||||
"description": "Select which tube lines you want to monitor",
|
||||
"data": {
|
||||
"line": "Tube lines"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configure London Underground",
|
||||
"description": "[%key:component::london_underground::config::step::user::description%]",
|
||||
"data": {
|
||||
"line": "[%key:component::london_underground::config::step::user::data::line%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue": {
|
||||
"title": "London Underground YAML configuration deprecated",
|
||||
"description": "Configuring London Underground using YAML sensor platform is deprecated.\n\nWhile importing your configuration, an error occurred when trying to connect to the Transport for London API. Please restart Home Assistant to try again, or remove the existing YAML configuration and set the integration up via the UI."
|
||||
}
|
||||
}
|
||||
}
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -367,6 +367,7 @@ FLOWS = {
|
||||
"local_ip",
|
||||
"local_todo",
|
||||
"locative",
|
||||
"london_underground",
|
||||
"lookin",
|
||||
"loqed",
|
||||
"luftdaten",
|
||||
|
||||
@@ -3688,9 +3688,10 @@
|
||||
},
|
||||
"london_underground": {
|
||||
"name": "London Underground",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"single_config_entry": true
|
||||
},
|
||||
"lookin": {
|
||||
"name": "LOOKin",
|
||||
|
||||
65
tests/components/london_underground/conftest.py
Normal file
65
tests/components/london_underground/conftest.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Fixtures for the london_underground tests."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from london_tube_status import parse_api_response
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.london_underground.const import CONF_LINE, DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, async_load_fixture
|
||||
from tests.conftest import AiohttpClientMocker
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry():
|
||||
"""Prevent setup of integration during tests."""
|
||||
with patch(
|
||||
"homeassistant.components.london_underground.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Mock the config entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={},
|
||||
options={CONF_LINE: ["Metropolitan"]},
|
||||
title="London Underground",
|
||||
)
|
||||
# Add and set up the entry
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
return entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_london_underground_client(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> AsyncGenerator[AsyncMock]:
|
||||
"""Mock a London Underground client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.london_underground.TubeData",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.london_underground.config_flow.TubeData",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
|
||||
# Load the fixture text
|
||||
fixture_text = await async_load_fixture(hass, "line_status.json", DOMAIN)
|
||||
fixture_data = parse_api_response(json.loads(fixture_text))
|
||||
client.data = fixture_data
|
||||
|
||||
yield client
|
||||
186
tests/components/london_underground/test_config_flow.py
Normal file
186
tests/components/london_underground/test_config_flow.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""Test the London Underground config flow."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.london_underground.const import (
|
||||
CONF_LINE,
|
||||
DEFAULT_LINES,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
|
||||
async def test_validate_input_success(
|
||||
hass: HomeAssistant, mock_setup_entry, mock_london_underground_client
|
||||
) -> None:
|
||||
"""Test successful validation of TfL API."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_LINE: ["Bakerloo", "Central"]},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "London Underground"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {CONF_LINE: ["Bakerloo", "Central"]}
|
||||
|
||||
|
||||
async def test_options(
|
||||
hass: HomeAssistant, mock_setup_entry, mock_config_entry
|
||||
) -> None:
|
||||
"""Test updating options."""
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_LINE: ["Bakerloo", "Central"],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_LINE: ["Bakerloo", "Central"],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_error"),
|
||||
[
|
||||
(Exception, "cannot_connect"),
|
||||
(asyncio.TimeoutError, "timeout_connect"),
|
||||
],
|
||||
)
|
||||
async def test_validate_input_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry,
|
||||
mock_london_underground_client,
|
||||
side_effect,
|
||||
expected_error,
|
||||
) -> None:
|
||||
"""Test validation with connection and timeout errors."""
|
||||
|
||||
mock_london_underground_client.update.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_LINE: ["Bakerloo", "Central"]},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"]["base"] == expected_error
|
||||
|
||||
# confirm recovery after error
|
||||
mock_london_underground_client.update.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "London Underground"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {CONF_LINE: DEFAULT_LINES}
|
||||
|
||||
|
||||
async def test_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_london_underground_client,
|
||||
mock_setup_entry,
|
||||
mock_config_entry,
|
||||
) -> None:
|
||||
"""Try (and fail) setting up a config entry when one already exists."""
|
||||
|
||||
# Try to start the flow
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
|
||||
async def test_yaml_import(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_london_underground_client,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a YAML sensor is imported and becomes an operational config entry."""
|
||||
# Set up via YAML which will trigger import and set up the config entry
|
||||
IMPORT_DATA = {
|
||||
"platform": "london_underground",
|
||||
"line": ["Central", "Piccadilly", "Victoria", "Bakerloo", "Northern"],
|
||||
}
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_DATA
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "London Underground"
|
||||
assert result["data"] == {}
|
||||
assert result["options"] == {
|
||||
CONF_LINE: ["Central", "Piccadilly", "Victoria", "Bakerloo", "Northern"]
|
||||
}
|
||||
|
||||
|
||||
async def test_failed_yaml_import_connection(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_london_underground_client,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a YAML sensor is imported and becomes an operational config entry."""
|
||||
# Set up via YAML which will trigger import and set up the config entry
|
||||
mock_london_underground_client.update.side_effect = asyncio.TimeoutError
|
||||
IMPORT_DATA = {
|
||||
"platform": "london_underground",
|
||||
"line": ["Central", "Piccadilly", "Victoria", "Bakerloo", "Northern"],
|
||||
}
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_DATA
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_failed_yaml_import_already_configured(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_london_underground_client,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_config_entry,
|
||||
) -> None:
|
||||
"""Test a YAML sensor is imported and becomes an operational config entry."""
|
||||
# Set up via YAML which will trigger import and set up the config entry
|
||||
|
||||
IMPORT_DATA = {
|
||||
"platform": "london_underground",
|
||||
"line": ["Central", "Piccadilly", "Victoria", "Bakerloo", "Northern"],
|
||||
}
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=IMPORT_DATA
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
20
tests/components/london_underground/test_init.py
Normal file
20
tests/components/london_underground/test_init.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Test the London Underground init."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def test_reload_entry(
|
||||
hass: HomeAssistant, mock_london_underground_client, mock_config_entry
|
||||
) -> None:
|
||||
"""Test reloading the config entry."""
|
||||
|
||||
# Test reloading with updated options
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry,
|
||||
data={},
|
||||
options={"line": ["Bakerloo", "Central"]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify that setup was called for each reload
|
||||
assert len(mock_london_underground_client.mock_calls) > 0
|
||||
@@ -1,37 +1,130 @@
|
||||
"""The tests for the london_underground platform."""
|
||||
|
||||
from london_tube_status import API_URL
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.london_underground.const import CONF_LINE, DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import async_load_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
VALID_CONFIG = {
|
||||
"sensor": {"platform": "london_underground", CONF_LINE: ["Metropolitan"]}
|
||||
}
|
||||
|
||||
|
||||
async def test_valid_state(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_london_underground_client,
|
||||
mock_config_entry,
|
||||
) -> None:
|
||||
"""Test for operational london_underground sensor with proper attributes."""
|
||||
aioclient_mock.get(
|
||||
API_URL,
|
||||
text=await async_load_fixture(hass, "line_status.json", DOMAIN),
|
||||
)
|
||||
"""Test operational London Underground sensor using a mock config entry."""
|
||||
# Ensure the entry is fully loaded
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Confirm that the expected entity exists and is correct
|
||||
state = hass.states.get("sensor.london_underground_metropolitan")
|
||||
assert state is not None
|
||||
assert state.state == "Good Service"
|
||||
assert state.attributes == {
|
||||
"Description": "Nothing to report",
|
||||
"attribution": "Powered by TfL Open Data",
|
||||
"friendly_name": "London Underground Metropolitan",
|
||||
"icon": "mdi:subway",
|
||||
}
|
||||
|
||||
# No YAML warning should be issued, since setup was not via YAML
|
||||
assert not issue_registry.async_get_issue(DOMAIN, "yaml_deprecated")
|
||||
|
||||
|
||||
async def test_yaml_import(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_london_underground_client,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a YAML sensor is imported and becomes an operational config entry."""
|
||||
# Set up via YAML which will trigger import and set up the config entry
|
||||
VALID_CONFIG = {
|
||||
"sensor": {
|
||||
"platform": "london_underground",
|
||||
CONF_LINE: ["Metropolitan", "London Overground"],
|
||||
}
|
||||
}
|
||||
assert await async_setup_component(hass, "sensor", VALID_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.metropolitan")
|
||||
# Verify the config entry was created
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
|
||||
# Verify a warning was issued about YAML deprecation
|
||||
assert issue_registry.async_get_issue(HOMEASSISTANT_DOMAIN, "deprecated_yaml")
|
||||
|
||||
# Check the state after setup completes
|
||||
state = hass.states.get("sensor.london_underground_metropolitan")
|
||||
assert state
|
||||
assert state.state == "Good Service"
|
||||
assert state.attributes == {
|
||||
"Description": "Nothing to report",
|
||||
"attribution": "Powered by TfL Open Data",
|
||||
"friendly_name": "Metropolitan",
|
||||
"friendly_name": "London Underground Metropolitan",
|
||||
"icon": "mdi:subway",
|
||||
}
|
||||
|
||||
# Since being renamed London overground is no longer returned by the API
|
||||
# So check that we do not import it and that we warn the user
|
||||
state = hass.states.get("sensor.london_underground_london_overground")
|
||||
assert not state
|
||||
assert any(
|
||||
"London Overground was removed from the configuration as the line has been divided and renamed"
|
||||
in record.message
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
|
||||
async def test_failed_yaml_import(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
mock_london_underground_client,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test a YAML sensor is imported and becomes an operational config entry."""
|
||||
# Set up via YAML which will trigger import and set up the config entry
|
||||
mock_london_underground_client.update.side_effect = asyncio.TimeoutError
|
||||
VALID_CONFIG = {
|
||||
"sensor": {"platform": "london_underground", CONF_LINE: ["Metropolitan"]}
|
||||
}
|
||||
assert await async_setup_component(hass, "sensor", VALID_CONFIG)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify the config entry was not created
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 0
|
||||
|
||||
# verify no flows still in progress
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 0
|
||||
|
||||
assert any(
|
||||
"Unexpected error trying to connect before importing config" in record.message
|
||||
for record in caplog.records
|
||||
)
|
||||
# Confirm that the import did not happen
|
||||
assert not any(
|
||||
"Importing London Underground config from configuration.yaml" in record.message
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
assert not any(
|
||||
"migrated to a config entry and can be safely removed" in record.message
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
# Verify a warning was issued about YAML not being imported
|
||||
assert issue_registry.async_get_issue(
|
||||
DOMAIN, "deprecated_yaml_import_issue_cannot_connect"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user