Remove deprecated YAML import from nederlandse_spoorwegen (#164662)

This commit is contained in:
epenet
2026-03-03 17:59:29 +01:00
committed by GitHub
parent 7496406156
commit 05acba37c7
6 changed files with 9 additions and 378 deletions
@@ -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(