mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 23:41:48 -05:00
Remove deprecated YAML import from nederlandse_spoorwegen (#164662)
This commit is contained in:
@@ -17,7 +17,6 @@ from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
ConfigSubentryData,
|
||||
ConfigSubentryFlow,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
@@ -30,15 +29,7 @@ from homeassistant.helpers.selector import (
|
||||
TimeSelector,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_FROM,
|
||||
CONF_ROUTES,
|
||||
CONF_TIME,
|
||||
CONF_TO,
|
||||
CONF_VIA,
|
||||
DOMAIN,
|
||||
INTEGRATION_TITLE,
|
||||
)
|
||||
from .const import CONF_FROM, CONF_TIME, CONF_TO, CONF_VIA, DOMAIN, INTEGRATION_TITLE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -133,47 +124,6 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle import from YAML configuration."""
|
||||
self._async_abort_entries_match({CONF_API_KEY: import_data[CONF_API_KEY]})
|
||||
|
||||
client = NSAPI(import_data[CONF_API_KEY])
|
||||
try:
|
||||
stations = await self.hass.async_add_executor_job(client.get_stations)
|
||||
except HTTPError:
|
||||
return self.async_abort(reason="invalid_auth")
|
||||
except RequestsConnectionError, Timeout:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception validating API key")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
station_codes = {station.code for station in stations}
|
||||
|
||||
subentries: list[ConfigSubentryData] = []
|
||||
for route in import_data.get(CONF_ROUTES, []):
|
||||
# Convert station codes to uppercase for consistency with UI routes
|
||||
for key in (CONF_FROM, CONF_TO, CONF_VIA):
|
||||
if key in route:
|
||||
route[key] = route[key].upper()
|
||||
if route[key] not in station_codes:
|
||||
return self.async_abort(reason="invalid_station")
|
||||
|
||||
subentries.append(
|
||||
ConfigSubentryData(
|
||||
title=route[CONF_NAME],
|
||||
subentry_type="route",
|
||||
data=route,
|
||||
unique_id=None,
|
||||
)
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=INTEGRATION_TITLE,
|
||||
data={CONF_API_KEY: import_data[CONF_API_KEY]},
|
||||
subentries=subentries,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
|
||||
@@ -12,7 +12,6 @@ AMS_TZ = ZoneInfo("Europe/Amsterdam")
|
||||
# Update every 2 minutes
|
||||
SCAN_INTERVAL = timedelta(minutes=2)
|
||||
|
||||
CONF_ROUTES = "routes"
|
||||
CONF_FROM = "from"
|
||||
CONF_TO = "to"
|
||||
CONF_VIA = "via"
|
||||
|
||||
@@ -5,42 +5,24 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ns_api import Trip
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME, EntityCategory
|
||||
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.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .binary_sensor import get_delay
|
||||
from .const import (
|
||||
CONF_FROM,
|
||||
CONF_ROUTES,
|
||||
CONF_TIME,
|
||||
CONF_TO,
|
||||
CONF_VIA,
|
||||
DOMAIN,
|
||||
INTEGRATION_TITLE,
|
||||
ROUTE_MODEL,
|
||||
)
|
||||
from .const import DOMAIN, INTEGRATION_TITLE, ROUTE_MODEL
|
||||
from .coordinator import NSConfigEntry, NSDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -70,26 +52,9 @@ TRIP_STATUS = {
|
||||
"CANCELLED": "cancelled",
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0 # since we use coordinator pattern
|
||||
|
||||
ROUTE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_FROM): cv.string,
|
||||
vol.Required(CONF_TO): cv.string,
|
||||
vol.Optional(CONF_VIA): cv.string,
|
||||
vol.Optional(CONF_TIME): cv.time,
|
||||
}
|
||||
)
|
||||
|
||||
ROUTES_SCHEMA = vol.All(cv.ensure_list, [ROUTE_SCHEMA])
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_ROUTES): ROUTES_SCHEMA}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class NSSensorEntityDescription(SensorEntityDescription):
|
||||
@@ -195,55 +160,6 @@ SENSOR_DESCRIPTIONS: tuple[NSSensorEntityDescription, ...] = (
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the departure sensor."""
|
||||
|
||||
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')}",
|
||||
breaks_in_ha_version="2026.4.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=f"deprecated_yaml_import_issue_{result.get('reason')}",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": INTEGRATION_TITLE,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
"deprecated_yaml",
|
||||
breaks_in_ha_version="2026.4.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": INTEGRATION_TITLE,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: NSConfigEntry,
|
||||
|
||||
@@ -127,23 +127,5 @@
|
||||
"name": "Transfers"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Configuring Nederlandse Spoorwegen using YAML sensor platform is deprecated.\n\nWhile importing your configuration, Home Assistant could not connect to the NS API. Please check your internet connection and the status of the NS API, then restart Home Assistant to try again, or remove the existing YAML configuration and set the integration up via the UI.",
|
||||
"title": "[%key:component::nederlandse_spoorwegen::issues::deprecated_yaml_import_issue_invalid_auth::title%]"
|
||||
},
|
||||
"deprecated_yaml_import_issue_invalid_auth": {
|
||||
"description": "Configuring Nederlandse Spoorwegen using YAML sensor platform is deprecated.\n\nWhile importing your configuration, an invalid API key was found. Please update your YAML configuration, or remove the existing YAML configuration and set the integration up via the UI.",
|
||||
"title": "Nederlandse Spoorwegen YAML configuration deprecated"
|
||||
},
|
||||
"deprecated_yaml_import_issue_invalid_station": {
|
||||
"description": "Configuring Nederlandse Spoorwegen using YAML sensor platform is deprecated.\n\nWhile importing your configuration an invalid station was found. Please update your YAML configuration, or remove the existing YAML configuration and set the integration up via the UI.",
|
||||
"title": "[%key:component::nederlandse_spoorwegen::issues::deprecated_yaml_import_issue_invalid_auth::title%]"
|
||||
},
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"description": "Configuring Nederlandse Spoorwegen using YAML sensor platform is deprecated.\n\nWhile importing your configuration, an unknown error occurred. Please restart Home Assistant to try again, or remove the existing YAML configuration and set the integration up via the UI.",
|
||||
"title": "[%key:component::nederlandse_spoorwegen::issues::deprecated_yaml_import_issue_invalid_auth::title%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Test config flow for Nederlandse Spoorwegen integration."""
|
||||
|
||||
from datetime import time
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
@@ -9,13 +7,12 @@ from requests import ConnectionError as RequestsConnectionError, HTTPError, Time
|
||||
|
||||
from homeassistant.components.nederlandse_spoorwegen.const import (
|
||||
CONF_FROM,
|
||||
CONF_ROUTES,
|
||||
CONF_TIME,
|
||||
CONF_TO,
|
||||
CONF_VIA,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_RECONFIGURE, SOURCE_USER
|
||||
from homeassistant.config_entries import SOURCE_RECONFIGURE, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@@ -165,174 +162,6 @@ async def test_already_configured(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_config_flow_import_success(
|
||||
hass: HomeAssistant, mock_nsapi: AsyncMock, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test successful import flow from YAML configuration."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={CONF_API_KEY: API_KEY},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Nederlandse Spoorwegen"
|
||||
assert result["data"] == {CONF_API_KEY: API_KEY}
|
||||
assert not result["result"].subentries
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("routes_data", "expected_routes_data"),
|
||||
[
|
||||
(
|
||||
# Test with uppercase station codes (UI behavior)
|
||||
[
|
||||
{
|
||||
CONF_NAME: "Home to Work",
|
||||
CONF_FROM: "ASD",
|
||||
CONF_TO: "RTD",
|
||||
CONF_VIA: "HT",
|
||||
CONF_TIME: time(hour=8, minute=30),
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
CONF_NAME: "Home to Work",
|
||||
CONF_FROM: "ASD",
|
||||
CONF_TO: "RTD",
|
||||
CONF_VIA: "HT",
|
||||
CONF_TIME: time(hour=8, minute=30),
|
||||
}
|
||||
],
|
||||
),
|
||||
(
|
||||
# Test with lowercase station codes (converted to uppercase)
|
||||
[
|
||||
{
|
||||
CONF_NAME: "Rotterdam-Amsterdam",
|
||||
CONF_FROM: "rtd", # lowercase input
|
||||
CONF_TO: "asd", # lowercase input
|
||||
},
|
||||
{
|
||||
CONF_NAME: "Amsterdam-Haarlem",
|
||||
CONF_FROM: "asd", # lowercase input
|
||||
CONF_TO: "ht", # lowercase input
|
||||
CONF_VIA: "rtd", # lowercase input
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
CONF_NAME: "Rotterdam-Amsterdam",
|
||||
CONF_FROM: "RTD", # converted to uppercase
|
||||
CONF_TO: "ASD", # converted to uppercase
|
||||
},
|
||||
{
|
||||
CONF_NAME: "Amsterdam-Haarlem",
|
||||
CONF_FROM: "ASD", # converted to uppercase
|
||||
CONF_TO: "HT", # converted to uppercase
|
||||
CONF_VIA: "RTD", # converted to uppercase
|
||||
},
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_config_flow_import_with_routes(
|
||||
hass: HomeAssistant,
|
||||
mock_nsapi: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
routes_data: list[dict[str, Any]],
|
||||
expected_routes_data: list[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Test import flow with routes from YAML configuration."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: API_KEY,
|
||||
CONF_ROUTES: routes_data,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Nederlandse Spoorwegen"
|
||||
assert result["data"] == {CONF_API_KEY: API_KEY}
|
||||
assert len(result["result"].subentries) == len(expected_routes_data)
|
||||
|
||||
subentries = list(result["result"].subentries.values())
|
||||
for expected_route in expected_routes_data:
|
||||
route_entry = next(
|
||||
entry for entry in subentries if entry.title == expected_route[CONF_NAME]
|
||||
)
|
||||
assert route_entry.data == expected_route
|
||||
assert route_entry.subentry_type == "route"
|
||||
|
||||
|
||||
async def test_config_flow_import_with_unknown_station(
|
||||
hass: HomeAssistant, mock_nsapi: AsyncMock, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test import flow aborts with unknown station in routes."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: API_KEY,
|
||||
CONF_ROUTES: [
|
||||
{
|
||||
CONF_NAME: "Home to Work",
|
||||
CONF_FROM: "HRM",
|
||||
CONF_TO: "RTD",
|
||||
CONF_VIA: "HT",
|
||||
CONF_TIME: time(hour=8, minute=30),
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "invalid_station"
|
||||
|
||||
|
||||
async def test_config_flow_import_already_configured(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test import flow when integration is already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={CONF_API_KEY: API_KEY},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_error"),
|
||||
[
|
||||
(HTTPError("Invalid API key"), "invalid_auth"),
|
||||
(Timeout("Cannot connect"), "cannot_connect"),
|
||||
(RequestsConnectionError("Cannot connect"), "cannot_connect"),
|
||||
(Exception("Unexpected error"), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_import_flow_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_nsapi: AsyncMock,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Test config flow handling different exceptions."""
|
||||
mock_nsapi.get_stations.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_API_KEY: API_KEY}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == expected_error
|
||||
|
||||
|
||||
async def test_reconfigure_success(
|
||||
hass: HomeAssistant, mock_nsapi: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
|
||||
@@ -11,7 +11,6 @@ from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.nederlandse_spoorwegen.const import (
|
||||
CONF_FROM,
|
||||
CONF_ROUTES,
|
||||
CONF_TIME,
|
||||
CONF_TO,
|
||||
CONF_VIA,
|
||||
@@ -19,19 +18,10 @@ from homeassistant.components.nederlandse_spoorwegen.const import (
|
||||
INTEGRATION_TITLE,
|
||||
SUBENTRY_TYPE_ROUTE,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.config_entries import ConfigSubentryDataWithId
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
import homeassistant.helpers.issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import setup_integration
|
||||
from .const import API_KEY
|
||||
@@ -49,41 +39,6 @@ def mock_sensor_platform() -> Generator:
|
||||
yield mock_platform
|
||||
|
||||
|
||||
async def test_config_import(
|
||||
hass: HomeAssistant,
|
||||
mock_nsapi,
|
||||
mock_setup_entry: AsyncMock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test sensor initialization."""
|
||||
await async_setup_component(
|
||||
hass,
|
||||
SENSOR_DOMAIN,
|
||||
{
|
||||
SENSOR_DOMAIN: [
|
||||
{
|
||||
CONF_PLATFORM: DOMAIN,
|
||||
CONF_API_KEY: API_KEY,
|
||||
CONF_ROUTES: [
|
||||
{
|
||||
CONF_NAME: "Spoorwegen Nederlande Station",
|
||||
CONF_FROM: "ASD",
|
||||
CONF_TO: "RTD",
|
||||
CONF_VIA: "HT",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(issue_registry.issues) == 1
|
||||
assert (HOMEASSISTANT_DOMAIN, "deprecated_yaml") in issue_registry.issues
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2025-09-15 14:30:00+00:00")
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensor(
|
||||
|
||||
Reference in New Issue
Block a user