Skip to content
Draft
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
13ac3cf
Adding tests to the existing NS integration
heindrichpaul Jun 27, 2025
1239ec9
Using a fixed now in the tests for stability
heindrichpaul Jun 27, 2025
4f1f081
Adding tests for the existing sensors
heindrichpaul Jun 27, 2025
796eb5e
Added final tests
heindrichpaul Jun 27, 2025
a9f48aa
Added config flow
heindrichpaul Jun 27, 2025
84e3f47
Added config flow
heindrichpaul Jun 27, 2025
d25bab9
Merge branch 'feature/improving_NS_integration' of github.com:heindri…
heindrichpaul Jun 27, 2025
f378ca3
Added tests and UI config flow to integration
heindrichpaul Jun 30, 2025
1720b5c
Fixed config flow
heindrichpaul Jun 30, 2025
54683e0
Fixed test
heindrichpaul Jun 30, 2025
474ce35
Fixed translation. Added more tests
heindrichpaul Jun 30, 2025
c3f387e
Merge pull request #2 from heindrichpaul/feature/improving_NS_integra…
heindrichpaul Jun 30, 2025
eba5047
Merge branch 'home-assistant:dev' into dev
heindrichpaul Jun 30, 2025
a2db49f
Added unique entity id's
heindrichpaul Jun 30, 2025
a81fcf3
updated the quality status
heindrichpaul Jul 1, 2025
73b0691
Updated the quality status
heindrichpaul Jul 3, 2025
ef6e055
Updated the quality status
heindrichpaul Jul 3, 2025
2999b35
Merge branch 'home-assistant:dev' into dev
heindrichpaul Jul 3, 2025
6607c23
Merge pull request #3 from heindrichpaul/feature/improving_NS_integra…
heindrichpaul Jul 3, 2025
394fd48
Merge pull request #4 from heindrichpaul/dev
heindrichpaul Jul 3, 2025
ef8b75d
Updated the config_flow to use const.py constants
heindrichpaul Jul 3, 2025
c867d19
Refactor Nederlandse Spoorwegen integration: update manifest formatti…
heindrichpaul Jul 3, 2025
9df3b57
Fixed order
heindrichpaul Jul 3, 2025
60cc1db
Refactor Nederlandse Spoorwegen integration: standardize titles and d…
heindrichpaul Jul 4, 2025
324ac65
Merge branch 'home-assistant:dev' into dev
heindrichpaul Jul 4, 2025
ade6a48
Merge branch 'dev' of github.com:heindrichpaul/core into feature/impr…
heindrichpaul Jul 4, 2025
a4448b0
Update homeassistant/components/nederlandse_spoorwegen/strings.json
heindrichpaul Jul 4, 2025
c23b2dc
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 4, 2025
ac1f29c
Update homeassistant/components/nederlandse_spoorwegen/strings.json
heindrichpaul Jul 4, 2025
bb56710
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 4, 2025
cdc6e9d
Update homeassistant/components/nederlandse_spoorwegen/strings.json
heindrichpaul Jul 4, 2025
938529f
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 4, 2025
3c65282
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 4, 2025
a2abe2c
removing un needed logging
heindrichpaul Jul 5, 2025
694075b
removing debugging logs
heindrichpaul Jul 5, 2025
0bf830f
Refactor Nederlandse Spoorwegen integration setup and improve error h…
heindrichpaul Jul 7, 2025
5da5cfa
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 7, 2025
ed6682d
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 7, 2025
b78cf88
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 7, 2025
68e2957
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 7, 2025
06e92c7
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 7, 2025
e1798f8
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 7, 2025
b2e567d
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 7, 2025
5f83cb4
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 7, 2025
5eff5fc
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 8, 2025
243ef7f
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 8, 2025
e8fbcdb
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 8, 2025
5e0ba85
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 9, 2025
f2cad24
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 9, 2025
0c5496e
Add unit tests for Nederlandse Spoorwegen integration
heindrichpaul Jul 10, 2025
10f88da
Cleaned up code
heindrichpaul Jul 10, 2025
ece9a9e
Remove unused stations attribute from coordinator and related tests
heindrichpaul Jul 10, 2025
5ffafb4
Cleaned strings.json
heindrichpaul Jul 10, 2025
0e600e3
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 10, 2025
892c1eb
Reverted
heindrichpaul Jul 10, 2025
a50f5f3
Merge branch 'feature/improving_NS_integration' of github.com:heindri…
heindrichpaul Jul 10, 2025
70362dd
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 11, 2025
17d396a
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 11, 2025
5afc872
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Jul 11, 2025
fbee5b8
Refactor Nederlandse Spoorwegen integration tests to utilize NSRuntim…
heindrichpaul Jul 14, 2025
ca26aab
Merge branch 'feature/improving_NS_integration' of github.com:heindri…
heindrichpaul Jul 14, 2025
7e4fa6a
Add migration tests for legacy routes to subentries in Nederlandse Sp…
heindrichpaul Jul 14, 2025
c1ffe7f
Add investigation scripts and tests for Nederlandse Spoorwegen integr…
heindrichpaul Jul 15, 2025
48c38ea
Refactor Nederlandse Spoorwegen tests and migration logic
heindrichpaul Jul 18, 2025
b4c99b9
Refactor Nederlandse Spoorwegen config flow by removing reauthenticat…
heindrichpaul Jul 18, 2025
93f9fb9
Add comprehensive tests for Nederlandse Spoorwegen sensors and utilities
heindrichpaul Jul 21, 2025
ff87004
Refactor Nederlandse Spoorwegen integration files for improved struct…
heindrichpaul Jul 21, 2025
db95fab
Refactor Nederlandse Spoorwegen diagnostics and test files for improv…
heindrichpaul Jul 21, 2025
c7ff0d9
Refactor Nederlandse Spoorwegen setup logic for improved error handli…
heindrichpaul Jul 21, 2025
50fb0e8
Add comment to clarify parallel update limit for Nederlandse Spoorweg…
heindrichpaul Jul 21, 2025
2f587c3
adding the import flow to the async setup
heindrichpaul Jul 21, 2025
f4e35bd
Fixed the import to use the platform_setup
heindrichpaul Jul 21, 2025
5fe9983
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Aug 5, 2025
e5a8cbf
Merge branch 'dev' into feature/improving_NS_integration
heindrichpaul Aug 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

196 changes: 196 additions & 0 deletions homeassistant/components/nederlandse_spoorwegen/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,197 @@
"""The nederlandse_spoorwegen component."""

from __future__ import annotations

import asyncio
import contextlib
from dataclasses import dataclass
import logging
from types import MappingProxyType
from typing import Any

from homeassistant.config_entries import ConfigEntry, ConfigSubentry
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType

from .api import NSAPIWrapper
from .const import CONF_FROM, CONF_ROUTES, CONF_TIME, CONF_TO, CONF_VIA, DOMAIN
from .coordinator import NSDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

# This integration can only be configured via config entries
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)


# Define runtime data structure for this integration
@dataclass
class NSRuntimeData:
"""Runtime data for the Nederlandse Spoorwegen integration."""

coordinator: NSDataUpdateCoordinator
stations: list[Any] | None = None # Full station objects with code and names
stations_updated: str | None = None


type NSConfigEntry = ConfigEntry[NSRuntimeData]

PLATFORMS = [Platform.SENSOR]


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Nederlandse Spoorwegen component."""
return True


async def async_setup_entry(hass: HomeAssistant, entry: NSConfigEntry) -> bool:
"""Set up Nederlandse Spoorwegen from a config entry."""
api_key = entry.data.get(CONF_API_KEY)
if not api_key:
raise ValueError("API key is required")

api_wrapper = NSAPIWrapper(hass, api_key)

coordinator = NSDataUpdateCoordinator(hass, api_wrapper, entry)

entry.runtime_data = NSRuntimeData(coordinator=coordinator)

await _async_migrate_legacy_routes(hass, entry)

entry.async_on_unload(entry.add_update_listener(async_reload_entry))

with contextlib.suppress(asyncio.CancelledError):
await coordinator.async_config_entry_first_refresh()

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload NS integration when options are updated."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, entry: NSConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


async def async_remove_entry(hass: HomeAssistant, entry: NSConfigEntry) -> None:
"""Handle removal of a config entry."""


async def _async_migrate_legacy_routes(
hass: HomeAssistant, entry: NSConfigEntry
) -> None:
"""Migrate legacy routes from configuration data into subentries.

This handles routes stored in entry.data[CONF_ROUTES] from legacy YAML config.
One-time migration to avoid duplicate imports.
"""
Comment on lines +70 to +77
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so instead of importing the yaml into a config entry and only then migrating that config entry into subentries, we should migrate to subentries directly instead. Less moving objects, less room for error :)

Copy link
Author

@heindrichpaul heindrichpaul Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually already doing exactly what you suggested. The current _async_migrate_legacy_routes function directly converts YAML routes to subentries without any intermediate steps:

  1. Legacy YAML routes (stored in entry.data[CONF_ROUTES])
  2. → Directly converted to ConfigSubentry objects
  3. → Added as subentries using hass.config_entries.async_add_subentry(entry, subentry)

There's no intermediate step of creating separate config entries first. The flow is:

  • Take YAML routes from entry.data[CONF_ROUTES]
  • Create ConfigSubentry objects directly from that YAML data
  • Add them as subentries to the existing config entry
  • Clean up the legacy CONF_ROUTES from the main entry data

@joostlek
Could you clarify if I'm missing something or if there's a specific aspect of the migration that should be improved?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no, you're running that code in async_setup_entry, which means that there already should be an entry to reach that code.

def setup_platform(
    hass: HomeAssistant,
    config: ConfigType,
    add_entities: AddEntitiesCallback,
    discovery_info: DiscoveryInfoType | None = None,
) -> None:

This is the current entrypoint of the integration and we should use this (or the async counterpart) to start an import flow, and then the code in async_step_import in the config flow should make sure all the right subentries are created.

Copy link
Author

@heindrichpaul heindrichpaul Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joostlek I have now added the logic to the async_setup as well. I have also run multiple tests locally and it imports the routes and config without issues

if entry.options.get("routes_migrated", False):
return

legacy_routes = entry.data.get(CONF_ROUTES, [])

hass.config_entries.async_update_entry(
entry, options={**entry.options, "routes_migrated": True}
)

if not legacy_routes:
return

migrated_count = 0

for route in legacy_routes:
try:
if not all(key in route for key in (CONF_NAME, CONF_FROM, CONF_TO)):
_LOGGER.warning(
"Skipping invalid route missing required fields: %s", route
)
continue

# Create subentry data with centralized station code normalization
subentry_data = {
CONF_NAME: route[CONF_NAME],
CONF_FROM: NSAPIWrapper.normalize_station_code(route[CONF_FROM]),
CONF_TO: NSAPIWrapper.normalize_station_code(route[CONF_TO]),
}

# Add optional fields if present
if route.get(CONF_VIA):
subentry_data[CONF_VIA] = NSAPIWrapper.normalize_station_code(
route[CONF_VIA]
)

if route.get(CONF_TIME):
subentry_data[CONF_TIME] = route[CONF_TIME]

# Create unique_id with centralized station code normalization
unique_id_parts = [
NSAPIWrapper.normalize_station_code(route[CONF_FROM]),
NSAPIWrapper.normalize_station_code(route[CONF_TO]),
NSAPIWrapper.normalize_station_code(route.get(CONF_VIA, "")),
]
unique_id = "_".join(part for part in unique_id_parts if part)

# Create the subentry
subentry = ConfigSubentry(
data=MappingProxyType(subentry_data),
subentry_type="route",
title=route[CONF_NAME],
unique_id=unique_id,
)

# Add the subentry to the config entry
hass.config_entries.async_add_subentry(entry, subentry)
migrated_count += 1

except (KeyError, ValueError) as ex:
_LOGGER.warning(
"Error migrating route %s: %s", route.get(CONF_NAME, "unknown"), ex
)

# Clean up legacy routes from data
new_data = {**entry.data}
if CONF_ROUTES in new_data:
new_data.pop(CONF_ROUTES)

# Update the config entry to remove legacy routes
hass.config_entries.async_update_entry(entry, data=new_data)


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)

if config_entry.version > 1:
# This means the user has downgraded from a future version
return False

if config_entry.version == 1:
new_data = {**config_entry.data}

if config_entry.minor_version < 1:
# Future migrations can be added here for schema changes
pass

# Update the config entry with new data and version
hass.config_entries.async_update_entry(
config_entry,
data=new_data,
minor_version=1,
version=1,
)

_LOGGER.debug(
"Migration to configuration version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
Loading