Remove NMBS YAML import (#145733)

* Remove NMBS YAML import

* Remove NMBS YAML import
This commit is contained in:
Joost Lekkerkerker 2025-06-02 15:10:46 +02:00 committed by GitHub
parent eb53277fcc
commit 434179ab3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 5 additions and 359 deletions

View File

@ -7,8 +7,6 @@ from pyrail.models import StationDetails
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import Platform
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import ( from homeassistant.helpers.selector import (
BooleanSelector, BooleanSelector,
@ -22,7 +20,6 @@ from .const import (
CONF_EXCLUDE_VIAS, CONF_EXCLUDE_VIAS,
CONF_SHOW_ON_MAP, CONF_SHOW_ON_MAP,
CONF_STATION_FROM, CONF_STATION_FROM,
CONF_STATION_LIVE,
CONF_STATION_TO, CONF_STATION_TO,
DOMAIN, DOMAIN,
) )
@ -115,68 +112,6 @@ class NMBSConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult:
"""Import configuration from yaml."""
try:
self.stations = await self._fetch_stations()
except CannotConnect:
return self.async_abort(reason="api_unavailable")
station_from = None
station_to = None
station_live = None
for station in self.stations:
if user_input[CONF_STATION_FROM] in (
station.standard_name,
station.name,
):
station_from = station
if user_input[CONF_STATION_TO] in (
station.standard_name,
station.name,
):
station_to = station
if CONF_STATION_LIVE in user_input and user_input[CONF_STATION_LIVE] in (
station.standard_name,
station.name,
):
station_live = station
if station_from is None or station_to is None:
return self.async_abort(reason="invalid_station")
if station_from == station_to:
return self.async_abort(reason="same_station")
# config flow uses id and not the standard name
user_input[CONF_STATION_FROM] = station_from.id
user_input[CONF_STATION_TO] = station_to.id
if station_live:
user_input[CONF_STATION_LIVE] = station_live.id
entity_registry = er.async_get(self.hass)
prefix = "live"
vias = "_excl_vias" if user_input.get(CONF_EXCLUDE_VIAS, False) else ""
if entity_id := entity_registry.async_get_entity_id(
Platform.SENSOR,
DOMAIN,
f"{prefix}_{station_live.standard_name}_{station_from.standard_name}_{station_to.standard_name}",
):
new_unique_id = f"{DOMAIN}_{prefix}_{station_live.id}_{station_from.id}_{station_to.id}{vias}"
entity_registry.async_update_entity(
entity_id, new_unique_id=new_unique_id
)
if entity_id := entity_registry.async_get_entity_id(
Platform.SENSOR,
DOMAIN,
f"{prefix}_{station_live.name}_{station_from.name}_{station_to.name}",
):
new_unique_id = f"{DOMAIN}_{prefix}_{station_live.id}_{station_from.id}_{station_to.id}{vias}"
entity_registry.async_update_entity(
entity_id, new_unique_id=new_unique_id
)
return await self.async_step_user(user_input)
class CannotConnect(Exception): class CannotConnect(Exception):
"""Error to indicate we cannot connect to NMBS.""" """Error to indicate we cannot connect to NMBS."""

View File

@ -8,30 +8,19 @@ from typing import Any
from pyrail import iRail from pyrail import iRail
from pyrail.models import ConnectionDetails, LiveboardDeparture, StationDetails from pyrail.models import ConnectionDetails, LiveboardDeparture, StationDetails
import voluptuous as vol
from homeassistant.components.sensor import ( from homeassistant.components.sensor import SensorEntity
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA, from homeassistant.config_entries import ConfigEntry
SensorEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_LATITUDE, ATTR_LATITUDE,
ATTR_LONGITUDE, ATTR_LONGITUDE,
CONF_NAME, CONF_NAME,
CONF_PLATFORM,
CONF_SHOW_ON_MAP, CONF_SHOW_ON_MAP,
UnitOfTime, UnitOfTime,
) )
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import ( from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import ( # noqa: F401 from .const import ( # noqa: F401
@ -47,22 +36,9 @@ from .const import ( # noqa: F401
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "NMBS"
DEFAULT_ICON = "mdi:train" DEFAULT_ICON = "mdi:train"
DEFAULT_ICON_ALERT = "mdi:alert-octagon" DEFAULT_ICON_ALERT = "mdi:alert-octagon"
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_STATION_FROM): cv.string,
vol.Required(CONF_STATION_TO): cv.string,
vol.Optional(CONF_STATION_LIVE): cv.string,
vol.Optional(CONF_EXCLUDE_VIAS, default=False): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
}
)
def get_time_until(departure_time: datetime | None = None): def get_time_until(departure_time: datetime | None = None):
"""Calculate the time between now and a train's departure time.""" """Calculate the time between now and a train's departure time."""
@ -85,71 +61,6 @@ def get_ride_duration(departure_time: datetime, arrival_time: datetime, delay=0)
return duration_time + get_delay_in_minutes(delay) return duration_time + get_delay_in_minutes(delay)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the NMBS sensor with iRail API."""
if config[CONF_PLATFORM] == DOMAIN:
if CONF_SHOW_ON_MAP not in config:
config[CONF_SHOW_ON_MAP] = False
if CONF_EXCLUDE_VIAS not in config:
config[CONF_EXCLUDE_VIAS] = False
station_types = [CONF_STATION_FROM, CONF_STATION_TO, CONF_STATION_LIVE]
for station_type in station_types:
station = (
find_station_by_name(hass, config[station_type])
if station_type in config
else None
)
if station is None and station_type in config:
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_import_issue_station_not_found",
breaks_in_ha_version="2025.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml_import_issue_station_not_found",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "NMBS",
"station_name": config[station_type],
"url": "/config/integrations/dashboard/add?domain=nmbs",
},
)
return
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
)
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2025.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "NMBS",
},
)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: ConfigEntry,

View File

@ -25,11 +25,5 @@
} }
} }
} }
},
"issues": {
"deprecated_yaml_import_issue_station_not_found": {
"title": "The {integration_title} YAML configuration import failed",
"description": "Configuring {integration_title} using YAML is being removed but there was a problem importing your YAML configuration.\n\nThe used station \"{station_name}\" could not be found. Fix it or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
}
} }
} }

View File

@ -3,21 +3,16 @@
from typing import Any from typing import Any
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.nmbs.config_flow import CONF_EXCLUDE_VIAS from homeassistant.components.nmbs.config_flow import CONF_EXCLUDE_VIAS
from homeassistant.components.nmbs.const import ( from homeassistant.components.nmbs.const import (
CONF_STATION_FROM, CONF_STATION_FROM,
CONF_STATION_LIVE,
CONF_STATION_TO, CONF_STATION_TO,
DOMAIN, DOMAIN,
) )
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_USER
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -150,192 +145,3 @@ async def test_unavailable_api(
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "api_unavailable" assert result["reason"] == "api_unavailable"
async def test_import(
hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_setup_entry: AsyncMock
) -> None:
"""Test starting a flow by user which filled in data for connection."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"],
CONF_STATION_LIVE: DUMMY_DATA_IMPORT["STAT_BRUSSELS_CENTRAL"],
CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"],
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert (
result["title"]
== "Train from Brussel-Noord/Bruxelles-Nord to Brussel-Zuid/Bruxelles-Midi"
)
assert result["data"] == {
CONF_STATION_FROM: "BE.NMBS.008812005",
CONF_STATION_LIVE: "BE.NMBS.008813003",
CONF_STATION_TO: "BE.NMBS.008814001",
}
assert (
result["result"].unique_id
== f"{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_{DUMMY_DATA['STAT_BRUSSELS_SOUTH']}"
)
async def test_step_import_abort_if_already_setup(
hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_config_entry: MockConfigEntry
) -> None:
"""Test starting a flow by user which filled in data for connection for already existing connection."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"],
CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"],
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_unavailable_api_import(
hass: HomeAssistant, mock_nmbs_client: AsyncMock
) -> None:
"""Test starting a flow by import and api is unavailable."""
mock_nmbs_client.get_stations.return_value = None
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"],
CONF_STATION_LIVE: DUMMY_DATA_IMPORT["STAT_BRUSSELS_CENTRAL"],
CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"],
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "api_unavailable"
@pytest.mark.parametrize(
("config", "reason"),
[
(
{
CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"],
CONF_STATION_TO: "Utrecht Centraal",
},
"invalid_station",
),
(
{
CONF_STATION_FROM: "Utrecht Centraal",
CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"],
},
"invalid_station",
),
(
{
CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"],
CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"],
},
"same_station",
),
],
)
async def test_invalid_station_name(
hass: HomeAssistant,
mock_nmbs_client: AsyncMock,
config: dict[str, Any],
reason: str,
) -> None:
"""Test importing invalid YAML."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data=config,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == reason
async def test_sensor_id_migration_standardname(
hass: HomeAssistant,
mock_nmbs_client: AsyncMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating unique id."""
old_unique_id = (
f"live_{DUMMY_DATA_IMPORT['STAT_BRUSSELS_NORTH']}_"
f"{DUMMY_DATA_IMPORT['STAT_BRUSSELS_NORTH']}_"
f"{DUMMY_DATA_IMPORT['STAT_BRUSSELS_SOUTH']}"
)
new_unique_id = (
f"nmbs_live_{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_"
f"{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_"
f"{DUMMY_DATA['STAT_BRUSSELS_SOUTH']}"
)
old_entry = entity_registry.async_get_or_create(
SENSOR_DOMAIN, DOMAIN, old_unique_id
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_STATION_LIVE: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"],
CONF_STATION_FROM: DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"],
CONF_STATION_TO: DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"],
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
config_entry_id = result["result"].entry_id
await hass.async_block_till_done()
entities = er.async_entries_for_config_entry(entity_registry, config_entry_id)
assert len(entities) == 3
entities_map = {entity.unique_id: entity for entity in entities}
assert old_unique_id not in entities_map
assert new_unique_id in entities_map
assert entities_map[new_unique_id].id == old_entry.id
async def test_sensor_id_migration_localized_name(
hass: HomeAssistant,
mock_nmbs_client: AsyncMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test migrating unique id."""
old_unique_id = (
f"live_{DUMMY_DATA_ALTERNATIVE_IMPORT['STAT_BRUSSELS_NORTH']}_"
f"{DUMMY_DATA_ALTERNATIVE_IMPORT['STAT_BRUSSELS_NORTH']}_"
f"{DUMMY_DATA_ALTERNATIVE_IMPORT['STAT_BRUSSELS_SOUTH']}"
)
new_unique_id = (
f"nmbs_live_{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_"
f"{DUMMY_DATA['STAT_BRUSSELS_NORTH']}_"
f"{DUMMY_DATA['STAT_BRUSSELS_SOUTH']}"
)
old_entry = entity_registry.async_get_or_create(
SENSOR_DOMAIN, DOMAIN, old_unique_id
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={
CONF_STATION_LIVE: DUMMY_DATA_ALTERNATIVE_IMPORT["STAT_BRUSSELS_NORTH"],
CONF_STATION_FROM: DUMMY_DATA_ALTERNATIVE_IMPORT["STAT_BRUSSELS_NORTH"],
CONF_STATION_TO: DUMMY_DATA_ALTERNATIVE_IMPORT["STAT_BRUSSELS_SOUTH"],
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
config_entry_id = result["result"].entry_id
await hass.async_block_till_done()
entities = er.async_entries_for_config_entry(entity_registry, config_entry_id)
assert len(entities) == 3
entities_map = {entity.unique_id: entity for entity in entities}
assert old_unique_id not in entities_map
assert new_unique_id in entities_map
assert entities_map[new_unique_id].id == old_entry.id