mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 01:07:10 +00:00
Add config flow to NMBS (#121548)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
parent
22b84450e8
commit
b9259b6f77
1
CODEOWNERS
generated
1
CODEOWNERS
generated
@ -1025,6 +1025,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/nina/ @DeerMaximum
|
/tests/components/nina/ @DeerMaximum
|
||||||
/homeassistant/components/nissan_leaf/ @filcole
|
/homeassistant/components/nissan_leaf/ @filcole
|
||||||
/homeassistant/components/nmbs/ @thibmaek
|
/homeassistant/components/nmbs/ @thibmaek
|
||||||
|
/tests/components/nmbs/ @thibmaek
|
||||||
/homeassistant/components/noaa_tides/ @jdelaney72
|
/homeassistant/components/noaa_tides/ @jdelaney72
|
||||||
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
||||||
/tests/components/nobo_hub/ @echoromeo @oyvindwe
|
/tests/components/nobo_hub/ @echoromeo @oyvindwe
|
||||||
|
@ -1 +1,45 @@
|
|||||||
"""The nmbs component."""
|
"""The NMBS component."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyrail import iRail
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the NMBS component."""
|
||||||
|
|
||||||
|
api_client = iRail()
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
station_response = await hass.async_add_executor_job(api_client.get_stations)
|
||||||
|
if station_response == -1:
|
||||||
|
return False
|
||||||
|
hass.data[DOMAIN] = station_response["station"]
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up NMBS from a config entry."""
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
180
homeassistant/components/nmbs/config_flow.py
Normal file
180
homeassistant/components/nmbs/config_flow.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
"""Config flow for NMBS integration."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pyrail import iRail
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
BooleanSelector,
|
||||||
|
SelectOptionDict,
|
||||||
|
SelectSelector,
|
||||||
|
SelectSelectorConfig,
|
||||||
|
SelectSelectorMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
CONF_EXCLUDE_VIAS,
|
||||||
|
CONF_SHOW_ON_MAP,
|
||||||
|
CONF_STATION_FROM,
|
||||||
|
CONF_STATION_LIVE,
|
||||||
|
CONF_STATION_TO,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NMBSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""NMBS config flow."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize."""
|
||||||
|
self.api_client = iRail()
|
||||||
|
self.stations: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
async def _fetch_stations(self) -> list[dict[str, Any]]:
|
||||||
|
"""Fetch the stations."""
|
||||||
|
stations_response = await self.hass.async_add_executor_job(
|
||||||
|
self.api_client.get_stations
|
||||||
|
)
|
||||||
|
if stations_response == -1:
|
||||||
|
raise CannotConnect("The API is currently unavailable.")
|
||||||
|
return stations_response["station"]
|
||||||
|
|
||||||
|
async def _fetch_stations_choices(self) -> list[SelectOptionDict]:
|
||||||
|
"""Fetch the stations options."""
|
||||||
|
|
||||||
|
if len(self.stations) == 0:
|
||||||
|
self.stations = await self._fetch_stations()
|
||||||
|
|
||||||
|
return [
|
||||||
|
SelectOptionDict(value=station["id"], label=station["standardname"])
|
||||||
|
for station in self.stations
|
||||||
|
]
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the step to setup a connection between 2 stations."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
choices = await self._fetch_stations_choices()
|
||||||
|
except CannotConnect:
|
||||||
|
return self.async_abort(reason="api_unavailable")
|
||||||
|
|
||||||
|
errors: dict = {}
|
||||||
|
if user_input is not None:
|
||||||
|
if user_input[CONF_STATION_FROM] == user_input[CONF_STATION_TO]:
|
||||||
|
errors["base"] = "same_station"
|
||||||
|
else:
|
||||||
|
[station_from] = [
|
||||||
|
station
|
||||||
|
for station in self.stations
|
||||||
|
if station["id"] == user_input[CONF_STATION_FROM]
|
||||||
|
]
|
||||||
|
[station_to] = [
|
||||||
|
station
|
||||||
|
for station in self.stations
|
||||||
|
if station["id"] == user_input[CONF_STATION_TO]
|
||||||
|
]
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
f"{user_input[CONF_STATION_FROM]}_{user_input[CONF_STATION_TO]}"
|
||||||
|
)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
config_entry_name = f"Train from {station_from["standardname"]} to {station_to["standardname"]}"
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=config_entry_name,
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
schema = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_STATION_FROM): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=choices,
|
||||||
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vol.Required(CONF_STATION_TO): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=choices,
|
||||||
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_EXCLUDE_VIAS): BooleanSelector(),
|
||||||
|
vol.Optional(CONF_SHOW_ON_MAP): BooleanSelector(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=schema,
|
||||||
|
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["standardname"],
|
||||||
|
station["name"],
|
||||||
|
):
|
||||||
|
station_from = station
|
||||||
|
if user_input[CONF_STATION_TO] in (
|
||||||
|
station["standardname"],
|
||||||
|
station["name"],
|
||||||
|
):
|
||||||
|
station_to = station
|
||||||
|
if CONF_STATION_LIVE in user_input and user_input[CONF_STATION_LIVE] in (
|
||||||
|
station["standardname"],
|
||||||
|
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"
|
||||||
|
if entity_id := entity_registry.async_get_entity_id(
|
||||||
|
Platform.SENSOR,
|
||||||
|
DOMAIN,
|
||||||
|
f"{prefix}_{station_live["standardname"]}_{station_from["standardname"]}_{station_to["standardname"]}",
|
||||||
|
):
|
||||||
|
new_unique_id = f"{DOMAIN}_{prefix}_{station_live["id"]}_{station_from["id"]}_{station_to["id"]}"
|
||||||
|
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"]}"
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
entity_id, new_unique_id=new_unique_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(Exception):
|
||||||
|
"""Error to indicate we cannot connect to NMBS."""
|
36
homeassistant/components/nmbs/const.py
Normal file
36
homeassistant/components/nmbs/const.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""The NMBS integration."""
|
||||||
|
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
DOMAIN: Final = "nmbs"
|
||||||
|
|
||||||
|
PLATFORMS: Final = [Platform.SENSOR]
|
||||||
|
|
||||||
|
CONF_STATION_FROM = "station_from"
|
||||||
|
CONF_STATION_TO = "station_to"
|
||||||
|
CONF_STATION_LIVE = "station_live"
|
||||||
|
CONF_EXCLUDE_VIAS = "exclude_vias"
|
||||||
|
CONF_SHOW_ON_MAP = "show_on_map"
|
||||||
|
|
||||||
|
|
||||||
|
def find_station_by_name(hass: HomeAssistant, station_name: str):
|
||||||
|
"""Find given station_name in the station list."""
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
s
|
||||||
|
for s in hass.data[DOMAIN]
|
||||||
|
if station_name in (s["standardname"], s["name"])
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_station(hass: HomeAssistant, station_name: str):
|
||||||
|
"""Find given station_id in the station list."""
|
||||||
|
return next(
|
||||||
|
(s for s in hass.data[DOMAIN] if station_name in s["id"]),
|
||||||
|
None,
|
||||||
|
)
|
@ -2,6 +2,7 @@
|
|||||||
"domain": "nmbs",
|
"domain": "nmbs",
|
||||||
"name": "NMBS",
|
"name": "NMBS",
|
||||||
"codeowners": ["@thibmaek"],
|
"codeowners": ["@thibmaek"],
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/nmbs",
|
"documentation": "https://www.home-assistant.io/integrations/nmbs",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyrail"],
|
"loggers": ["pyrail"],
|
||||||
|
@ -11,19 +11,33 @@ from homeassistant.components.sensor import (
|
|||||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||||
SensorEntity,
|
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 HomeAssistant
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
from .const import ( # noqa: F401
|
||||||
|
CONF_EXCLUDE_VIAS,
|
||||||
|
CONF_STATION_FROM,
|
||||||
|
CONF_STATION_LIVE,
|
||||||
|
CONF_STATION_TO,
|
||||||
|
DOMAIN,
|
||||||
|
PLATFORMS,
|
||||||
|
find_station,
|
||||||
|
find_station_by_name,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
API_FAILURE = -1
|
API_FAILURE = -1
|
||||||
@ -33,11 +47,6 @@ DEFAULT_NAME = "NMBS"
|
|||||||
DEFAULT_ICON = "mdi:train"
|
DEFAULT_ICON = "mdi:train"
|
||||||
DEFAULT_ICON_ALERT = "mdi:alert-octagon"
|
DEFAULT_ICON_ALERT = "mdi:alert-octagon"
|
||||||
|
|
||||||
CONF_STATION_FROM = "station_from"
|
|
||||||
CONF_STATION_TO = "station_to"
|
|
||||||
CONF_STATION_LIVE = "station_live"
|
|
||||||
CONF_EXCLUDE_VIAS = "exclude_vias"
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_STATION_FROM): cv.string,
|
vol.Required(CONF_STATION_FROM): cv.string,
|
||||||
@ -73,33 +82,97 @@ def get_ride_duration(departure_time, arrival_time, delay=0):
|
|||||||
return duration_time + get_delay_in_minutes(delay)
|
return duration_time + get_delay_in_minutes(delay)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
async def async_setup_platform(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the NMBS sensor with iRail API."""
|
"""Set up the NMBS sensor with iRail API."""
|
||||||
|
|
||||||
api_client = iRail()
|
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
|
||||||
|
|
||||||
name = config[CONF_NAME]
|
station_types = [CONF_STATION_FROM, CONF_STATION_TO, CONF_STATION_LIVE]
|
||||||
show_on_map = config[CONF_SHOW_ON_MAP]
|
|
||||||
station_from = config[CONF_STATION_FROM]
|
|
||||||
station_to = config[CONF_STATION_TO]
|
|
||||||
station_live = config.get(CONF_STATION_LIVE)
|
|
||||||
excl_vias = config[CONF_EXCLUDE_VIAS]
|
|
||||||
|
|
||||||
sensors: list[SensorEntity] = [
|
for station_type in station_types:
|
||||||
NMBSSensor(api_client, name, show_on_map, station_from, station_to, excl_vias)
|
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
|
||||||
|
|
||||||
if station_live is not None:
|
hass.async_create_task(
|
||||||
sensors.append(
|
hass.config_entries.flow.async_init(
|
||||||
NMBSLiveBoard(api_client, station_live, station_from, station_to)
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=config,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
add_entities(sensors, True)
|
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(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up NMBS sensor entities based on a config entry."""
|
||||||
|
api_client = iRail()
|
||||||
|
|
||||||
|
name = config_entry.data.get(CONF_NAME, None)
|
||||||
|
show_on_map = config_entry.data.get(CONF_SHOW_ON_MAP, False)
|
||||||
|
excl_vias = config_entry.data.get(CONF_EXCLUDE_VIAS, False)
|
||||||
|
|
||||||
|
station_from = find_station(hass, config_entry.data[CONF_STATION_FROM])
|
||||||
|
station_to = find_station(hass, config_entry.data[CONF_STATION_TO])
|
||||||
|
|
||||||
|
# setup the connection from station to station
|
||||||
|
# setup a disabled liveboard for both from and to station
|
||||||
|
async_add_entities(
|
||||||
|
[
|
||||||
|
NMBSSensor(
|
||||||
|
api_client, name, show_on_map, station_from, station_to, excl_vias
|
||||||
|
),
|
||||||
|
NMBSLiveBoard(api_client, station_from, station_from, station_to),
|
||||||
|
NMBSLiveBoard(api_client, station_to, station_from, station_to),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NMBSLiveBoard(SensorEntity):
|
class NMBSLiveBoard(SensorEntity):
|
||||||
@ -116,16 +189,18 @@ class NMBSLiveBoard(SensorEntity):
|
|||||||
self._attrs = {}
|
self._attrs = {}
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
|
self.entity_registry_enabled_default = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the sensor default name."""
|
"""Return the sensor default name."""
|
||||||
return f"NMBS Live ({self._station})"
|
return f"Trains in {self._station["standardname"]}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self):
|
def unique_id(self):
|
||||||
"""Return a unique ID."""
|
"""Return the unique ID."""
|
||||||
unique_id = f"{self._station}_{self._station_from}_{self._station_to}"
|
|
||||||
|
|
||||||
|
unique_id = f"{self._station}_{self._station_from}_{self._station_to}"
|
||||||
return f"nmbs_live_{unique_id}"
|
return f"nmbs_live_{unique_id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -155,7 +230,7 @@ class NMBSLiveBoard(SensorEntity):
|
|||||||
"departure_minutes": departure,
|
"departure_minutes": departure,
|
||||||
"extra_train": int(self._attrs["isExtra"]) > 0,
|
"extra_train": int(self._attrs["isExtra"]) > 0,
|
||||||
"vehicle_id": self._attrs["vehicle"],
|
"vehicle_id": self._attrs["vehicle"],
|
||||||
"monitored_station": self._station,
|
"monitored_station": self._station["standardname"],
|
||||||
}
|
}
|
||||||
|
|
||||||
if delay > 0:
|
if delay > 0:
|
||||||
@ -166,7 +241,7 @@ class NMBSLiveBoard(SensorEntity):
|
|||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
"""Set the state equal to the next departure."""
|
"""Set the state equal to the next departure."""
|
||||||
liveboard = self._api_client.get_liveboard(self._station)
|
liveboard = self._api_client.get_liveboard(self._station["id"])
|
||||||
|
|
||||||
if liveboard == API_FAILURE:
|
if liveboard == API_FAILURE:
|
||||||
_LOGGER.warning("API failed in NMBSLiveBoard")
|
_LOGGER.warning("API failed in NMBSLiveBoard")
|
||||||
@ -209,8 +284,17 @@ class NMBSSensor(SensorEntity):
|
|||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def unique_id(self) -> str:
|
||||||
|
"""Return the unique ID."""
|
||||||
|
unique_id = f"{self._station_from["id"]}_{self._station_to["id"]}"
|
||||||
|
|
||||||
|
return f"nmbs_connection_{unique_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
|
if self._name is None:
|
||||||
|
return f"Train from {self._station_from["standardname"]} to {self._station_to["standardname"]}"
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -234,7 +318,7 @@ class NMBSSensor(SensorEntity):
|
|||||||
canceled = int(self._attrs["departure"]["canceled"])
|
canceled = int(self._attrs["departure"]["canceled"])
|
||||||
|
|
||||||
attrs = {
|
attrs = {
|
||||||
"destination": self._station_to,
|
"destination": self._attrs["departure"]["station"],
|
||||||
"direction": self._attrs["departure"]["direction"]["name"],
|
"direction": self._attrs["departure"]["direction"]["name"],
|
||||||
"platform_arriving": self._attrs["arrival"]["platform"],
|
"platform_arriving": self._attrs["arrival"]["platform"],
|
||||||
"platform_departing": self._attrs["departure"]["platform"],
|
"platform_departing": self._attrs["departure"]["platform"],
|
||||||
@ -296,7 +380,7 @@ class NMBSSensor(SensorEntity):
|
|||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
"""Set the state to the duration of a connection."""
|
"""Set the state to the duration of a connection."""
|
||||||
connections = self._api_client.get_connections(
|
connections = self._api_client.get_connections(
|
||||||
self._station_from, self._station_to
|
self._station_from["id"], self._station_to["id"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if connections == API_FAILURE:
|
if connections == API_FAILURE:
|
||||||
|
35
homeassistant/components/nmbs/strings.json
Normal file
35
homeassistant/components/nmbs/strings.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
||||||
|
"api_unavailable": "The API is currently unavailable.",
|
||||||
|
"same_station": "[%key:component::nmbs::config::error::same_station%]",
|
||||||
|
"invalid_station": "Invalid station."
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"same_station": "Departure and arrival station can not be the same."
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"station_from": "Departure station",
|
||||||
|
"station_to": "Arrival station",
|
||||||
|
"exclude_vias": "Direct connections only",
|
||||||
|
"show_on_map": "Display on map"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"station_from": "Station where the train departs",
|
||||||
|
"station_to": "Station where the train arrives",
|
||||||
|
"exclude_vias": "Exclude connections with transfers",
|
||||||
|
"show_on_map": "Show the station on the map"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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 an 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@ -416,6 +416,7 @@ FLOWS = {
|
|||||||
"niko_home_control",
|
"niko_home_control",
|
||||||
"nina",
|
"nina",
|
||||||
"nmap_tracker",
|
"nmap_tracker",
|
||||||
|
"nmbs",
|
||||||
"nobo_hub",
|
"nobo_hub",
|
||||||
"nordpool",
|
"nordpool",
|
||||||
"notion",
|
"notion",
|
||||||
|
@ -4224,7 +4224,7 @@
|
|||||||
"nmbs": {
|
"nmbs": {
|
||||||
"name": "NMBS",
|
"name": "NMBS",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"config_flow": false,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
"no_ip": {
|
"no_ip": {
|
||||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@ -1814,6 +1814,9 @@ pyps4-2ndscreen==1.3.1
|
|||||||
# homeassistant.components.qwikswitch
|
# homeassistant.components.qwikswitch
|
||||||
pyqwikswitch==0.93
|
pyqwikswitch==0.93
|
||||||
|
|
||||||
|
# homeassistant.components.nmbs
|
||||||
|
pyrail==0.0.3
|
||||||
|
|
||||||
# homeassistant.components.rainbird
|
# homeassistant.components.rainbird
|
||||||
pyrainbird==6.0.1
|
pyrainbird==6.0.1
|
||||||
|
|
||||||
|
20
tests/components/nmbs/__init__.py
Normal file
20
tests/components/nmbs/__init__.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"""Tests for the NMBS integration."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from tests.common import load_fixture
|
||||||
|
|
||||||
|
|
||||||
|
def mock_api_unavailable() -> dict[str, Any]:
|
||||||
|
"""Mock for unavailable api."""
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
def mock_station_response() -> dict[str, Any]:
|
||||||
|
"""Mock for valid station response."""
|
||||||
|
dummy_stations_response: dict[str, Any] = json.loads(
|
||||||
|
load_fixture("stations.json", "nmbs")
|
||||||
|
)
|
||||||
|
|
||||||
|
return dummy_stations_response
|
58
tests/components/nmbs/conftest.py
Normal file
58
tests/components/nmbs/conftest.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"""NMBS tests configuration."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.nmbs.const import (
|
||||||
|
CONF_STATION_FROM,
|
||||||
|
CONF_STATION_TO,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.nmbs.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_nmbs_client() -> Generator[AsyncMock]:
|
||||||
|
"""Mock a NMBS client."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.nmbs.iRail",
|
||||||
|
autospec=True,
|
||||||
|
) as mock_client,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.nmbs.config_flow.iRail",
|
||||||
|
new=mock_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
client = mock_client.return_value
|
||||||
|
client.get_stations.return_value = load_json_object_fixture(
|
||||||
|
"stations.json", DOMAIN
|
||||||
|
)
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Mock a config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="Train from Brussel-Noord/Bruxelles-Nord to Brussel-Zuid/Bruxelles-Midi",
|
||||||
|
data={
|
||||||
|
CONF_STATION_FROM: "BE.NMBS.008812005",
|
||||||
|
CONF_STATION_TO: "BE.NMBS.008814001",
|
||||||
|
},
|
||||||
|
unique_id="BE.NMBS.008812005_BE.NMBS.008814001",
|
||||||
|
)
|
30
tests/components/nmbs/fixtures/stations.json
Normal file
30
tests/components/nmbs/fixtures/stations.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"version": "1.3",
|
||||||
|
"timestamp": "1720252400",
|
||||||
|
"station": [
|
||||||
|
{
|
||||||
|
"@id": "http://irail.be/stations/NMBS/008812005",
|
||||||
|
"id": "BE.NMBS.008812005",
|
||||||
|
"name": "Brussels-North",
|
||||||
|
"locationX": "4.360846",
|
||||||
|
"locationY": "50.859663",
|
||||||
|
"standardname": "Brussel-Noord/Bruxelles-Nord"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "http://irail.be/stations/NMBS/008813003",
|
||||||
|
"id": "BE.NMBS.008813003",
|
||||||
|
"name": "Brussels-Central",
|
||||||
|
"locationX": "4.356801",
|
||||||
|
"locationY": "50.845658",
|
||||||
|
"standardname": "Brussel-Centraal/Bruxelles-Central"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@id": "http://irail.be/stations/NMBS/008814001",
|
||||||
|
"id": "BE.NMBS.008814001",
|
||||||
|
"name": "Brussels-South/Brussels-Midi",
|
||||||
|
"locationX": "4.336531",
|
||||||
|
"locationY": "50.835707",
|
||||||
|
"standardname": "Brussel-Zuid/Bruxelles-Midi"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
310
tests/components/nmbs/test_config_flow.py
Normal file
310
tests/components/nmbs/test_config_flow.py
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
"""Test the NMBS config flow."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components.nmbs.const import (
|
||||||
|
CONF_STATION_FROM,
|
||||||
|
CONF_STATION_LIVE,
|
||||||
|
CONF_STATION_TO,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_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 entity_registry as er
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
DUMMY_DATA_IMPORT: dict[str, Any] = {
|
||||||
|
"STAT_BRUSSELS_NORTH": "Brussel-Noord/Bruxelles-Nord",
|
||||||
|
"STAT_BRUSSELS_CENTRAL": "Brussel-Centraal/Bruxelles-Central",
|
||||||
|
"STAT_BRUSSELS_SOUTH": "Brussel-Zuid/Bruxelles-Midi",
|
||||||
|
}
|
||||||
|
|
||||||
|
DUMMY_DATA_ALTERNATIVE_IMPORT: dict[str, Any] = {
|
||||||
|
"STAT_BRUSSELS_NORTH": "Brussels-North",
|
||||||
|
"STAT_BRUSSELS_CENTRAL": "Brussels-Central",
|
||||||
|
"STAT_BRUSSELS_SOUTH": "Brussels-South/Brussels-Midi",
|
||||||
|
}
|
||||||
|
|
||||||
|
DUMMY_DATA: dict[str, Any] = {
|
||||||
|
"STAT_BRUSSELS_NORTH": "BE.NMBS.008812005",
|
||||||
|
"STAT_BRUSSELS_CENTRAL": "BE.NMBS.008813003",
|
||||||
|
"STAT_BRUSSELS_SOUTH": "BE.NMBS.008814001",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_flow(
|
||||||
|
hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_setup_entry: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test the full flow."""
|
||||||
|
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_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"],
|
||||||
|
CONF_STATION_TO: DUMMY_DATA["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: DUMMY_DATA["STAT_BRUSSELS_NORTH"],
|
||||||
|
CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_SOUTH"],
|
||||||
|
}
|
||||||
|
assert (
|
||||||
|
result["result"].unique_id
|
||||||
|
== f"{DUMMY_DATA["STAT_BRUSSELS_NORTH"]}_{DUMMY_DATA["STAT_BRUSSELS_SOUTH"]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_same_station(
|
||||||
|
hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_setup_entry: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test selecting the same station."""
|
||||||
|
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_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"],
|
||||||
|
CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_NORTH"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": "same_station"}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"],
|
||||||
|
CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_SOUTH"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_abort_if_exists(
|
||||||
|
hass: HomeAssistant, mock_nmbs_client: AsyncMock, mock_config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test aborting the flow if the entry already exists."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data={
|
||||||
|
CONF_STATION_FROM: DUMMY_DATA["STAT_BRUSSELS_NORTH"],
|
||||||
|
CONF_STATION_TO: DUMMY_DATA["STAT_BRUSSELS_SOUTH"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unavailable_api(
|
||||||
|
hass: HomeAssistant, mock_nmbs_client: AsyncMock
|
||||||
|
) -> None:
|
||||||
|
"""Test starting a flow by user and api is unavailable."""
|
||||||
|
mock_nmbs_client.get_stations.return_value = -1
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
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 == "BE.NMBS.008812005_BE.NMBS.008814001"
|
||||||
|
|
||||||
|
|
||||||
|
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 = -1
|
||||||
|
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,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test migrating unique id."""
|
||||||
|
entity_registry.async_get_or_create(
|
||||||
|
SENSOR_DOMAIN,
|
||||||
|
DOMAIN,
|
||||||
|
f"live_{DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"]}_{DUMMY_DATA_IMPORT["STAT_BRUSSELS_NORTH"]}_{DUMMY_DATA_IMPORT["STAT_BRUSSELS_SOUTH"]}",
|
||||||
|
config_entry=mock_config_entry,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
entities = er.async_entries_for_config_entry(
|
||||||
|
entity_registry, mock_config_entry.entry_id
|
||||||
|
)
|
||||||
|
assert len(entities) == 1
|
||||||
|
assert (
|
||||||
|
entities[0].unique_id
|
||||||
|
== f"nmbs_live_{DUMMY_DATA["STAT_BRUSSELS_NORTH"]}_{DUMMY_DATA["STAT_BRUSSELS_NORTH"]}_{DUMMY_DATA["STAT_BRUSSELS_SOUTH"]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensor_id_migration_localized_name(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_nmbs_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test migrating unique id."""
|
||||||
|
entity_registry.async_get_or_create(
|
||||||
|
SENSOR_DOMAIN,
|
||||||
|
DOMAIN,
|
||||||
|
f"live_{DUMMY_DATA_ALTERNATIVE_IMPORT["STAT_BRUSSELS_NORTH"]}_{DUMMY_DATA_ALTERNATIVE_IMPORT["STAT_BRUSSELS_NORTH"]}_{DUMMY_DATA_ALTERNATIVE_IMPORT["STAT_BRUSSELS_SOUTH"]}",
|
||||||
|
config_entry=mock_config_entry,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
entities = er.async_entries_for_config_entry(
|
||||||
|
entity_registry, mock_config_entry.entry_id
|
||||||
|
)
|
||||||
|
assert len(entities) == 1
|
||||||
|
assert (
|
||||||
|
entities[0].unique_id
|
||||||
|
== f"nmbs_live_{DUMMY_DATA["STAT_BRUSSELS_NORTH"]}_{DUMMY_DATA["STAT_BRUSSELS_NORTH"]}_{DUMMY_DATA["STAT_BRUSSELS_SOUTH"]}"
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user