diff --git a/homeassistant/components/nederlandse_spoorwegen/coordinator.py b/homeassistant/components/nederlandse_spoorwegen/coordinator.py index 0930915d69a..a7b736c322d 100644 --- a/homeassistant/components/nederlandse_spoorwegen/coordinator.py +++ b/homeassistant/components/nederlandse_spoorwegen/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta import logging from ns_api import NSAPI, Trip @@ -28,9 +28,17 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _now_nl() -> datetime: - """Return current time in Europe/Amsterdam timezone.""" - return dt_util.now(AMS_TZ) +def _current_time_nl(tomorrow: bool = False) -> datetime: + """Return current time for today or tomorrow in Europe/Amsterdam timezone.""" + now = dt_util.now(AMS_TZ) + if tomorrow: + now = now + timedelta(days=1) + return now + + +def _format_time(dt: datetime) -> str: + """Format datetime to NS API format (DD-MM-YYYY HH:MM).""" + return dt.strftime("%d-%m-%Y %H:%M") type NSConfigEntry = ConfigEntry[dict[str, NSDataUpdateCoordinator]] @@ -91,6 +99,13 @@ class NSDataUpdateCoordinator(DataUpdateCoordinator[NSRouteResult]): # Filter out trips that have already departed (trips are already sorted) future_trips = self._remove_trips_in_the_past(trips) + # If a specific time is configured, filter to only show trips at or after that time + if self.departure_time: + reference_time = self._get_time_from_route(self.departure_time) + future_trips = self._filter_trips_at_or_after_time( + future_trips, reference_time + ) + # Process trips to find current and next departure first_trip, next_trip = self._get_first_and_next_trips(future_trips) @@ -100,20 +115,34 @@ class NSDataUpdateCoordinator(DataUpdateCoordinator[NSRouteResult]): next_trip=next_trip, ) - def _get_time_from_route(self, time_str: str | None) -> str: - """Combine today's date with a time string if needed.""" + def _get_time_from_route(self, time_str: str | None) -> datetime: + """Convert time string to datetime with automatic rollover to tomorrow if needed.""" if not time_str: - return _now_nl().strftime("%d-%m-%Y %H:%M") + return _current_time_nl() if ( isinstance(time_str, str) and len(time_str.split(":")) in (2, 3) and " " not in time_str ): - today = _now_nl().strftime("%d-%m-%Y") - return f"{today} {time_str[:5]}" + # Parse time-only string (HH:MM or HH:MM:SS) + time_only = time_str[:5] # Take HH:MM only + hours, minutes = map(int, time_only.split(":")) + + # Create datetime with today's date and the specified time + now = _current_time_nl() + result_dt = now.replace(hour=hours, minute=minutes, second=0, microsecond=0) + + # If the time is more than 1 hour in the past, assume user meant tomorrow + if (now - result_dt).total_seconds() > 3600: + result_dt = _current_time_nl(tomorrow=True).replace( + hour=hours, minute=minutes, second=0, microsecond=0 + ) + + return result_dt + # Fallback: use current date and time - return _now_nl().strftime("%d-%m-%Y %H:%M") + return _current_time_nl() async def _get_trips( self, @@ -124,8 +153,9 @@ class NSDataUpdateCoordinator(DataUpdateCoordinator[NSRouteResult]): ) -> list[Trip]: """Get trips from NS API, sorted by departure time.""" - # Convert time to full date-time string if needed and default to Dutch local time if not provided - time_str = self._get_time_from_route(departure_time) + # Convert time to datetime with rollover logic, then format for API + reference_time = self._get_time_from_route(departure_time) + time_str = _format_time(reference_time) trips = await self.hass.async_add_executor_job( self.nsapi.get_trips, @@ -141,6 +171,10 @@ class NSDataUpdateCoordinator(DataUpdateCoordinator[NSRouteResult]): if not trips: return [] + return self._sort_trips_by_departure(trips) + + def _sort_trips_by_departure(self, trips: list[Trip]) -> list[Trip]: + """Sort trips by departure time (actual or planned).""" return sorted( trips, key=lambda trip: ( @@ -148,7 +182,7 @@ class NSDataUpdateCoordinator(DataUpdateCoordinator[NSRouteResult]): if trip.departure_time_actual is not None else trip.departure_time_planned if trip.departure_time_planned is not None - else _now_nl() + else _current_time_nl() ), ) @@ -170,7 +204,7 @@ class NSDataUpdateCoordinator(DataUpdateCoordinator[NSRouteResult]): def _remove_trips_in_the_past(self, trips: list[Trip]) -> list[Trip]: """Filter out trips that have already departed.""" # Compare against Dutch local time to align with ns_api timezone handling - now = _now_nl() + now = _current_time_nl() future_trips = [] for trip in trips: departure_time = ( @@ -189,6 +223,41 @@ class NSDataUpdateCoordinator(DataUpdateCoordinator[NSRouteResult]): future_trips.append(trip) return future_trips + def _filter_trips_at_or_after_time( + self, trips: list[Trip], reference_time: datetime + ) -> list[Trip]: + """Filter trips to only those at or after the reference time (ignoring date). + + The API returns trips spanning multiple days, so we simply filter + by time component to show only trips at or after the configured time. + """ + filtered_trips = [] + ref_time_only = reference_time.time() + + for trip in trips: + departure_time = ( + trip.departure_time_actual + if trip.departure_time_actual is not None + else trip.departure_time_planned + ) + + if departure_time is None: + continue + + # Make naive datetimes timezone-aware if needed + if ( + departure_time.tzinfo is None + or departure_time.tzinfo.utcoffset(departure_time) is None + ): + departure_time = departure_time.replace(tzinfo=reference_time.tzinfo) + + # Compare only the time component, ignoring the date + trip_time_only = departure_time.time() + if trip_time_only >= ref_time_only: + filtered_trips.append(trip) + + return filtered_trips + def _find_next_trip( self, future_trips: list[Trip], first_trip: Trip ) -> Trip | None: diff --git a/tests/components/nederlandse_spoorwegen/conftest.py b/tests/components/nederlandse_spoorwegen/conftest.py index 100a7205622..b798578e211 100644 --- a/tests/components/nederlandse_spoorwegen/conftest.py +++ b/tests/components/nederlandse_spoorwegen/conftest.py @@ -71,6 +71,14 @@ def mock_no_trips_nsapi(mock_nsapi: AsyncMock) -> Generator[AsyncMock]: return mock_nsapi +@pytest.fixture +def mock_tomorrow_trips_nsapi(mock_nsapi: AsyncMock) -> Generator[AsyncMock]: + """Override async_setup_entry.""" + trips_data = load_json_object_fixture("trip_tomorrow.json", DOMAIN) + mock_nsapi.get_trips.return_value = [Trip(trip) for trip in trips_data["trips"]] + return mock_nsapi + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Mock config entry.""" diff --git a/tests/components/nederlandse_spoorwegen/fixtures/trip_tomorrow.json b/tests/components/nederlandse_spoorwegen/fixtures/trip_tomorrow.json new file mode 100644 index 00000000000..d65e45da035 --- /dev/null +++ b/tests/components/nederlandse_spoorwegen/fixtures/trip_tomorrow.json @@ -0,0 +1,2770 @@ +{ + "source": "HARP", + "trips": [ + { + "idx": 0, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-16T08:24:00+02:00|plannedArrivalTime=2025-09-16T10:40:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=2333456918", + "ctxRecon": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-16T08:24:00+02:00|plannedArrivalTime=2025-09-16T10:40:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=2333456918", + "sourceCtxRecon": "\u00b6HKI\u00b6T$A=1@O=Amsterdam Centraal@L=1100850@a=128@$A=1@O=Utrecht Centraal@L=1100728@a=128@$202509161624$202509161651$IC 3059 $$1$$$$$$\u00a7W$A=1@O=Utrecht Centraal@L=1100728@a=128@$A=1@O=Utrecht Centraal@L=1100905@a=128@$202509161651$202509161653$$$1$$$$$$\u00a7T$A=1@O=Utrecht Centraal@L=1100905@a=128@$A=1@O='s-Hertogenbosch@L=1100870@a=128@$202509161654$202509161722$IC 3559 $$3$$$$$$\u00a7W$A=1@O='s-Hertogenbosch@L=1100870@a=128@$A=1@O='s-Hertogenbosch@L=1101032@a=128@$202509161722$202509161726$$$1$$$$$$\u00a7T$A=1@O='s-Hertogenbosch@L=1101032@a=128@$A=1@O=Utrecht Centraal@L=1100715@a=128@$202509161730$202509161757$IC 2760 $$3$$$$$$\u00a7W$A=1@O=Utrecht Centraal@L=1100715@a=128@$A=1@O=Utrecht Centraal@L=1100930@a=128@$202509161757$202509161802$$$1$$$$$$\u00a7T$A=1@O=Utrecht Centraal@L=1100930@a=128@$A=1@O=Rotterdam Centraal@L=1100690@a=128@$202509161803$202509161840$IC 2860 $$1$$$$$$\u00b6KC\u00b6#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#\u00b6KCC\u00b6#VE#0#ERG#45319#HIN#460#ECK#13954|13944|14077|14080|0|0|485|13938|1|0|8|0|0|-2147483648#\u00b6KRCC\u00b6#VE#1#MRTF#", + "plannedDurationInMinutes": 136, + "actualDurationInMinutes": 135, + "transfers": 3, + "status": "NORMAL", + "messages": [], + "legs": [ + { + "idx": "0", + "name": "IC 3059", + "travelType": "PUBLIC_TRANSIT", + "direction": "Nijmegen", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#444542#TA#0#DA#150925#1S#1101028#1T#1504#LS#1101149#LT#1747#PU#784#RT#1#CA#IC#ZE#3059#ZB#IC 3059 #PC#1#FR#1101028#FT#1504#TO#1101149#TT#1747#", + "origin": { + "name": "Amsterdam Centraal", + "lng": 4.90027761459351, + "lat": 52.3788871765137, + "countryCode": "NL", + "uicCode": "8400058", + "uicCdCode": "118400058", + "stationCode": "ASD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T08:24:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T08:25:00+0200", + "plannedTrack": "4b", + "actualTrack": "4b", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "Utrecht Centraal", + "lng": 5.11027765274048, + "lat": 52.0888900756836, + "countryCode": "NL", + "uicCode": "8400621", + "uicCdCode": "118400621", + "stationCode": "UT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T16:51:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T16:51:00+0200", + "plannedTrack": "19", + "actualTrack": "19", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3059", + "categoryCode": "IC", + "shortCategoryName": "IC", + "longCategoryName": "Intercity", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity", + "shortValue": "NS Intercity", + "accessibilityValue": "NS Intercity", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Nijmegen", + "shortValue": "richting Nijmegen", + "accessibilityValue": "richting Nijmegen", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "1 tussenstop", + "shortValue": "1 tussenstop", + "accessibilityValue": "1 tussenstop", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "stops": [ + { + "uicCode": "8400058", + "uicCdCode": "118400058", + "name": "Amsterdam Centraal", + "lat": 52.3788871765137, + "lng": 4.90027761459351, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-16T08:24:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-16T08:25:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "4b", + "plannedDepartureTrack": "4b", + "plannedArrivalTrack": "4b", + "actualArrivalTrack": "4b", + "departureDelayInSeconds": 60, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400057", + "uicCdCode": "118400057", + "name": "Amsterdam Amstel", + "lat": 52.3466682434082, + "lng": 4.91777801513672, + "countryCode": "NL", + "notes": [], + "routeIdx": 2, + "plannedDepartureDateTime": "2025-09-15T16:32:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16:33:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T16:31:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T16:32:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 60, + "arrivalDelayInSeconds": 60, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400621", + "uicCdCode": "118400621", + "name": "Utrecht Centraal", + "lat": 52.0888900756836, + "lng": 5.11027765274048, + "countryCode": "NL", + "notes": [], + "routeIdx": 10, + "plannedArrivalDateTime": "2025-09-15T16:51:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T16:51:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "19", + "plannedDepartureTrack": "19", + "plannedArrivalTrack": "19", + "actualArrivalTrack": "19", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "MEDIUM", + "bicycleSpotCount": 6, + "punctuality": 100.0, + "crossPlatformTransfer": true, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#444542#TA#0#DA#150925#1S#1101028#1T#1504#LS#1101149#LT#1747#PU#784#RT#1#CA#IC#ZE#3059#ZB#IC 3059 #PC#1#FR#1101028#FT#1504#TO#1101149#TT#1747#&train=3059&datetime=2025-09-16T08:24:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 27, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "26 min.", + "accessibilityValue": "26 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 2, + "distanceInMeters": 37529 + }, + { + "idx": "1", + "name": "IC 3559", + "travelType": "PUBLIC_TRANSIT", + "direction": "Venlo", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#507406#TA#0#DA#150925#1S#1101787#1T#1504#LS#1100958#LT#1828#PU#784#RT#3#CA#IC#ZE#3559#ZB#IC 3559 #PC#1#FR#1101787#FT#1504#TO#1100958#TT#1828#", + "origin": { + "name": "Utrecht Centraal", + "lng": 5.11027765274048, + "lat": 52.0888900756836, + "countryCode": "NL", + "uicCode": "8400621", + "uicCdCode": "118400621", + "stationCode": "UT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T16:54:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T16:54:00+0200", + "plannedTrack": "18", + "actualTrack": "18", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T17:22:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:22:00+0200", + "plannedTrack": "6", + "actualTrack": "6", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3559", + "categoryCode": "IC", + "shortCategoryName": "IC", + "longCategoryName": "Intercity", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity", + "shortValue": "NS Intercity", + "accessibilityValue": "NS Intercity", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Venlo", + "shortValue": "richting Venlo", + "accessibilityValue": "richting Venlo", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "Geen tussenstops", + "shortValue": "Geen tussenstops", + "accessibilityValue": "Geen tussenstops", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "transferMessages": [ + { + "message": "3 min. overstaptijd", + "accessibilityMessage": "3 minuten overstaptijd", + "type": "TRANSFER_TIME", + "messageNesProperties": { + "color": "text-default", + "type": "informative" + } + } + ], + "stops": [ + { + "uicCode": "8400621", + "uicCdCode": "118400621", + "name": "Utrecht Centraal", + "lat": 52.0888900756836, + "lng": 5.11027765274048, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-15T16:54:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16:54:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "18", + "plannedDepartureTrack": "18", + "plannedArrivalTrack": "18", + "actualArrivalTrack": "18", + "departureDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400319", + "uicCdCode": "118400319", + "name": "'s-Hertogenbosch", + "lat": 51.69048, + "lng": 5.29362, + "countryCode": "NL", + "notes": [], + "routeIdx": 8, + "plannedArrivalDateTime": "2025-09-15T17:22:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:22:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "6", + "plannedDepartureTrack": "6", + "plannedArrivalTrack": "6", + "actualArrivalTrack": "6", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "HIGH", + "bicycleSpotCount": 6, + "punctuality": 77.8, + "crossPlatformTransfer": false, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#507406#TA#0#DA#150925#1S#1101787#1T#1504#LS#1100958#LT#1828#PU#784#RT#3#CA#IC#ZE#3559#ZB#IC 3559 #PC#1#FR#1101787#FT#1504#TO#1100958#TT#1828#&train=3559&datetime=2025-09-15T16:54:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 28, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "28 min.", + "accessibilityValue": "28 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 4, + "distanceInMeters": 47266 + }, + { + "idx": "2", + "name": "IC 2760", + "travelType": "PUBLIC_TRANSIT", + "direction": "Alkmaar", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#505746#TA#0#DA#150925#1S#1101011#1T#1559#LS#1101009#LT#1903#PU#784#RT#3#CA#IC#ZE#2760#ZB#IC 2760 #PC#1#FR#1101011#FT#1559#TO#1101009#TT#1903#", + "origin": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T17:30:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:30:00+0200", + "plannedTrack": "3", + "actualTrack": "3", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "Utrecht Centraal", + "lng": 5.11027765274048, + "lat": 52.0888900756836, + "countryCode": "NL", + "uicCode": "8400621", + "uicCdCode": "118400621", + "stationCode": "UT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T17:57:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:57:00+0200", + "plannedTrack": "7", + "actualTrack": "7", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "2760", + "categoryCode": "IC", + "shortCategoryName": "IC", + "longCategoryName": "Intercity", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity", + "shortValue": "NS Intercity", + "accessibilityValue": "NS Intercity", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Alkmaar", + "shortValue": "richting Alkmaar", + "accessibilityValue": "richting Alkmaar", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "Geen tussenstops", + "shortValue": "Geen tussenstops", + "accessibilityValue": "Geen tussenstops", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "transferMessages": [ + { + "message": "8 min. overstaptijd", + "accessibilityMessage": "8 minuten overstaptijd", + "type": "TRANSFER_TIME", + "messageNesProperties": { + "color": "text-default", + "type": "informative" + } + } + ], + "stops": [ + { + "uicCode": "8400319", + "uicCdCode": "118400319", + "name": "'s-Hertogenbosch", + "lat": 51.69048, + "lng": 5.29362, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-15T17:30:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:30:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "3", + "plannedDepartureTrack": "3", + "plannedArrivalTrack": "3", + "actualArrivalTrack": "3", + "departureDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400621", + "uicCdCode": "118400621", + "name": "Utrecht Centraal", + "lat": 52.0888900756836, + "lng": 5.11027765274048, + "countryCode": "NL", + "notes": [], + "routeIdx": 8, + "plannedArrivalDateTime": "2025-09-15T17:57:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:57:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "7", + "plannedDepartureTrack": "7", + "plannedArrivalTrack": "7", + "actualArrivalTrack": "7", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "crossPlatformTransfer": false, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#505746#TA#0#DA#150925#1S#1101011#1T#1559#LS#1101009#LT#1903#PU#784#RT#3#CA#IC#ZE#2760#ZB#IC 2760 #PC#1#FR#1101011#FT#1559#TO#1101009#TT#1903#&train=2760&datetime=2025-09-15T17:30:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 27, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "27 min.", + "accessibilityValue": "27 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 5, + "distanceInMeters": 47266 + }, + { + "idx": "3", + "name": "IC 2860", + "travelType": "PUBLIC_TRANSIT", + "direction": "Rotterdam Centraal", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1180#TA#0#DA#150925#1S#1100930#1T#1803#LS#1100690#LT#1840#PU#784#RT#1#CA#IC#ZE#2860#ZB#IC 2860 #PC#1#FR#1100930#FT#1803#TO#1100690#TT#1840#", + "origin": { + "name": "Utrecht Centraal", + "lng": 5.11027765274048, + "lat": 52.0888900756836, + "countryCode": "NL", + "uicCode": "8400621", + "uicCdCode": "118400621", + "stationCode": "UT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T10:03:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T10:03:00+0200", + "plannedTrack": "12", + "actualTrack": "12", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "Rotterdam Centraal", + "lng": 4.46888875961304, + "lat": 51.9249992370605, + "countryCode": "NL", + "uicCode": "8400530", + "uicCdCode": "118400530", + "stationCode": "RTD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T10:40:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T10:40:00+0200", + "plannedTrack": "14", + "actualTrack": "14", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "2860", + "categoryCode": "IC", + "shortCategoryName": "IC", + "longCategoryName": "Intercity", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity", + "shortValue": "NS Intercity", + "accessibilityValue": "NS Intercity", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Rotterdam Centraal", + "shortValue": "richting Rotterdam Centraal", + "accessibilityValue": "richting Rotterdam Centraal", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "2 tussenstops", + "shortValue": "2 tussenstops", + "accessibilityValue": "2 tussenstops", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "transferMessages": [ + { + "message": "6 min. overstaptijd", + "accessibilityMessage": "6 minuten overstaptijd", + "type": "TRANSFER_TIME", + "messageNesProperties": { + "color": "text-default", + "type": "informative" + } + } + ], + "stops": [ + { + "uicCode": "8400621", + "uicCdCode": "118400621", + "name": "Utrecht Centraal", + "lat": 52.0888900756836, + "lng": 5.11027765274048, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-16T10:03:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-16T10:03:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "12", + "plannedDepartureTrack": "12", + "plannedArrivalTrack": "12", + "actualArrivalTrack": "12", + "departureDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400258", + "uicCdCode": "118400258", + "name": "Gouda", + "lat": 52.0175018310547, + "lng": 4.70444440841675, + "countryCode": "NL", + "notes": [], + "routeIdx": 6, + "plannedDepartureDateTime": "2025-09-16T10:22:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-16T10:22:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-16T10:21:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-16T10:21:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "8", + "plannedDepartureTrack": "8", + "plannedArrivalTrack": "8", + "actualArrivalTrack": "8", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400507", + "uicCdCode": "118400507", + "name": "Rotterdam Alexander", + "lat": 51.9519462585449, + "lng": 4.55361127853394, + "countryCode": "NL", + "notes": [], + "routeIdx": 9, + "plannedDepartureDateTime": "2025-09-16T10:31:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-16T10:31:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-16T10:31:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-16T10:31:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "2", + "plannedDepartureTrack": "2", + "plannedArrivalTrack": "2", + "actualArrivalTrack": "2", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400530", + "uicCdCode": "118400530", + "name": "Rotterdam Centraal", + "lat": 51.9249992370605, + "lng": 4.46888875961304, + "countryCode": "NL", + "notes": [], + "routeIdx": 11, + "plannedArrivalDateTime": "2025-09-16T10:40:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-16T10:40:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "14", + "plannedDepartureTrack": "14", + "plannedArrivalTrack": "14", + "actualArrivalTrack": "14", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 9, + "punctuality": 90.0, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1180#TA#0#DA#150925#1S#1100930#1T#1803#LS#1100690#LT#1840#PU#784#RT#1#CA#IC#ZE#2860#ZB#IC 2860 #PC#1#FR#1100930#FT#1803#TO#1100690#TT#1840#&train=2860&datetime=2025-09-16T10:03:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 37, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "37 min.", + "accessibilityValue": "37 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "distanceInMeters": 50987 + } + ], + "checksum": "2dbeefb4_3", + "crowdForecast": "HIGH", + "punctuality": 77.8, + "optimal": false, + "fares": [], + "fareLegs": [ + { + "origin": { + "name": "Amsterdam Centraal", + "lng": 4.90027761459351, + "lat": 52.3788871765137, + "countryCode": "NL", + "uicCode": "8400058", + "uicCdCode": "118400058", + "stationCode": "ASD", + "type": "STATION" + }, + "destination": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION" + }, + "operator": "NS", + "productTypes": ["TRAIN"], + "fares": [ + { + "priceInCents": 1910, + "priceInCentsExcludingSupplement": 1910, + "supplementInCents": 0, + "buyableTicketSupplementPriceInCents": 0, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + } + ], + "travelDate": "2025-09-15" + }, + { + "origin": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION" + }, + "destination": { + "name": "Rotterdam Centraal", + "lng": 4.46888875961304, + "lat": 51.9249992370605, + "countryCode": "NL", + "uicCode": "8400530", + "uicCdCode": "118400530", + "stationCode": "RTD", + "type": "STATION" + }, + "operator": "NS", + "productTypes": ["TRAIN"], + "fares": [ + { + "priceInCents": 2010, + "priceInCentsExcludingSupplement": 2010, + "supplementInCents": 0, + "buyableTicketSupplementPriceInCents": 0, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + } + ], + "travelDate": "2025-09-15" + } + ], + "productFare": { + "priceInCents": 3920, + "priceInCentsExcludingSupplement": 3920, + "buyableTicketPriceInCents": 3920, + "buyableTicketPriceInCentsExcludingSupplement": 3920, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + }, + "fareOptions": { + "isInternationalBookable": false, + "isInternational": false, + "isEticketBuyable": false, + "isPossibleWithOvChipkaart": false, + "isTotalPriceUnknown": false, + "reasonEticketNotBuyable": { + "reason": "VIA_STATION_REQUESTED", + "description": "Je kunt voor deze reis geen kaartje kopen, omdat je je reis via een extra station hebt gepland. Uiteraard kun je voor deze reis betalen met het saldo op je OV-chipkaart." + } + }, + "nsiLink": { + "url": "https://www.nsinternational.com/nl/treintickets-v3/#/search/ASD/RTD/20250915/1624/1840?stationType=domestic&cookieConsent=false", + "showInternationalBanner": false + }, + "type": "NS", + "shareUrl": { + "uri": "https://www.ns.nl/rpx?ctx=arnu%7CfromStation%3D8400058%7CrequestedFromStation%3D8400058%7CtoStation%3D8400530%7CrequestedToStation%3D8400530%7CviaStation%3D8400319%7CplannedFromTime%3D2025-09-15T16%3A24%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T18%3A40%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D2333456918" + }, + "realtime": true, + "registerJourney": { + "url": "https://treinwijzer.ns.nl/idp/login?ctxRecon=arnu%7CfromStation%3D8400058%7CrequestedFromStation%3D8400058%7CtoStation%3D8400530%7CrequestedToStation%3D8400530%7CviaStation%3D8400319%7CplannedFromTime%3D2025-09-15T16%3A24%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T18%3A40%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D2333456918&originUicCode=8400058&destinationUicCode=8400530&dateTime=2025-09-15T16%3A28%3A00.051873%2B02%3A00&searchForArrival=false&viaUicCode=8400319&excludeHighSpeedTrains=false&localTrainsOnly=false&searchForAccessibleTrip=false&lang=nl&travelAssistance=false", + "searchUrl": "https://treinwijzer.ns.nl/idp/login?search=true&originUicCode=8400058&destinationUicCode=8400530&dateTime=2025-09-15T16%3A28%3A00.051873%2B02%3A00&searchForArrival=false&viaUicCode=8400319&excludeHighSpeedTrains=false&localTrainsOnly=false&searchForAccessibleTrip=false&lang=nl&travelAssistance=false", + "status": "REGISTRATION_POSSIBLE", + "bicycleReservationRequired": false + }, + "modalityListItems": [ + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "4b", + "accessibilityName": "Intercity" + }, + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "18", + "accessibilityName": "Intercity" + }, + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "3", + "accessibilityName": "Intercity" + }, + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "12", + "accessibilityName": "Intercity" + } + ] + }, + { + "idx": 1, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-16T08:34:00+02:00|plannedArrivalTime=2025-09-16T10:37:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1180343963", + "ctxRecon": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-16T08:34:00+02:00|plannedArrivalTime=2025-09-16T10:37:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1180343963", + "sourceCtxRecon": "\u00b6HKI\u00b6T$A=1@O=Amsterdam Centraal@L=1100836@a=128@$A=1@O='s-Hertogenbosch@L=1100870@a=128@$202509161634$202509161731$IC 2761 $$1$$$$$$\u00a7W$A=1@O='s-Hertogenbosch@L=1100870@a=128@$A=1@O='s-Hertogenbosch@L=1101751@a=128@$202509161731$202509161733$$$1$$$$$$\u00a7T$A=1@O='s-Hertogenbosch@L=1101751@a=128@$A=1@O=Breda@L=1101034@a=128@$202509161741$202509161810$IC 3661 $$3$$$$$$\u00a7W$A=1@O=Breda@L=1101034@a=128@$A=1@O=Breda@L=1100942@a=128@$202509161810$202509161812$$$1$$$$$$\u00a7T$A=1@O=Breda@L=1100942@a=128@$A=1@O=Rotterdam Centraal@L=1100726@a=128@$202509161814$202509161837$ICD 1871$$1$$$$$$\u00b6KC\u00b6#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#\u00b6KCC\u00b6#VE#0#ERG#261#HIN#390#ECK#13954|13954|14077|14077|0|0|66021|13938|2|0|2|0|0|-2147483648#\u00b6KRCC\u00b6#VE#1#MRTF#", + "plannedDurationInMinutes": 123, + "actualDurationInMinutes": 122, + "transfers": 2, + "status": "NORMAL", + "messages": [], + "legs": [ + { + "idx": "0", + "name": "IC 2761", + "travelType": "PUBLIC_TRANSIT", + "direction": "Maastricht", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1088#TA#0#DA#150925#1S#1101009#1T#1557#LS#1101011#LT#1903#PU#784#RT#1#CA#IC#ZE#2761#ZB#IC 2761 #PC#1#FR#1101009#FT#1557#TO#1101011#TT#1903#", + "origin": { + "name": "Amsterdam Centraal", + "lng": 4.90027761459351, + "lat": 52.3788871765137, + "countryCode": "NL", + "uicCode": "8400058", + "uicCdCode": "118400058", + "stationCode": "ASD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T08:34:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T08:35:00+0200", + "plannedTrack": "4", + "actualTrack": "4", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T17:31:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:31:00+0200", + "plannedTrack": "6", + "actualTrack": "6", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "2761", + "categoryCode": "IC", + "shortCategoryName": "IC", + "longCategoryName": "Intercity", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity", + "shortValue": "NS Intercity", + "accessibilityValue": "NS Intercity", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Maastricht", + "shortValue": "richting Maastricht", + "accessibilityValue": "richting Maastricht", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "2 tussenstops", + "shortValue": "2 tussenstops", + "accessibilityValue": "2 tussenstops", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "stops": [ + { + "uicCode": "8400058", + "uicCdCode": "118400058", + "name": "Amsterdam Centraal", + "lat": 52.3788871765137, + "lng": 4.90027761459351, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-16T08:34:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-16T08:35:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 60, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400057", + "uicCdCode": "118400057", + "name": "Amsterdam Amstel", + "lat": 52.3466682434082, + "lng": 4.91777801513672, + "countryCode": "NL", + "notes": [], + "routeIdx": 2, + "plannedDepartureDateTime": "2025-09-15T16:42:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16:43:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T16:42:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T16:43:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 60, + "arrivalDelayInSeconds": 60, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400621", + "uicCdCode": "118400621", + "name": "Utrecht Centraal", + "lat": 52.0888900756836, + "lng": 5.11027765274048, + "countryCode": "NL", + "notes": [], + "routeIdx": 10, + "plannedDepartureDateTime": "2025-09-15T17:03:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:03:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:00:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:00:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "15", + "plannedDepartureTrack": "15", + "plannedArrivalTrack": "15", + "actualArrivalTrack": "15", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400319", + "uicCdCode": "118400319", + "name": "'s-Hertogenbosch", + "lat": 51.69048, + "lng": 5.29362, + "countryCode": "NL", + "notes": [], + "routeIdx": 18, + "plannedArrivalDateTime": "2025-09-15T17:31:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:31:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "6", + "plannedDepartureTrack": "6", + "plannedArrivalTrack": "6", + "actualArrivalTrack": "6", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "MEDIUM", + "bicycleSpotCount": 6, + "crossPlatformTransfer": true, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1088#TA#0#DA#150925#1S#1101009#1T#1557#LS#1101011#LT#1903#PU#784#RT#1#CA#IC#ZE#2761#ZB#IC 2761 #PC#1#FR#1101009#FT#1557#TO#1101011#TT#1903#&train=2761&datetime=2025-09-16T08:34:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 57, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "56 min.", + "accessibilityValue": "56 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 2, + "distanceInMeters": 84795 + }, + { + "idx": "1", + "name": "IC 3661", + "travelType": "PUBLIC_TRANSIT", + "direction": "Roosendaal", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#505945#TA#0#DA#150925#1S#1101167#1T#1550#LS#1101102#LT#1833#PU#784#RT#3#CA#IC#ZE#3661#ZB#IC 3661 #PC#1#FR#1101167#FT#1550#TO#1101102#TT#1833#", + "origin": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T17:41:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:41:00+0200", + "plannedTrack": "7", + "actualTrack": "7", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "Breda", + "lng": 4.78000020980835, + "lat": 51.5955543518066, + "countryCode": "NL", + "uicCode": "8400131", + "uicCdCode": "118400131", + "stationCode": "BD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T10:10:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T10:10:00+0200", + "plannedTrack": "8", + "actualTrack": "8", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3661", + "categoryCode": "IC", + "shortCategoryName": "IC", + "longCategoryName": "Intercity", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity", + "shortValue": "NS Intercity", + "accessibilityValue": "NS Intercity", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Roosendaal", + "shortValue": "richting Roosendaal", + "accessibilityValue": "richting Roosendaal", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "1 tussenstop", + "shortValue": "1 tussenstop", + "accessibilityValue": "1 tussenstop", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "transferMessages": [ + { + "message": "Overstap op zelfde perron", + "accessibilityMessage": "Overstap op zelfde perron", + "type": "CROSS_PLATFORM", + "messageNesProperties": { + "color": "text-default", + "type": "informative" + } + } + ], + "stops": [ + { + "uicCode": "8400319", + "uicCdCode": "118400319", + "name": "'s-Hertogenbosch", + "lat": 51.69048, + "lng": 5.29362, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-15T17:41:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:41:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "7", + "plannedDepartureTrack": "7", + "plannedArrivalTrack": "7", + "actualArrivalTrack": "7", + "departureDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400597", + "uicCdCode": "118400597", + "name": "Tilburg", + "lat": 51.5605545043945, + "lng": 5.08361101150513, + "countryCode": "NL", + "notes": [], + "routeIdx": 1, + "plannedDepartureDateTime": "2025-09-15T17:58:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:58:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:56:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:56:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "3", + "plannedDepartureTrack": "3", + "plannedArrivalTrack": "3", + "actualArrivalTrack": "3", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400131", + "uicCdCode": "118400131", + "name": "Breda", + "lat": 51.5955543518066, + "lng": 4.78000020980835, + "countryCode": "NL", + "notes": [], + "routeIdx": 5, + "plannedArrivalDateTime": "2025-09-16T10:10:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-16T10:10:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "8", + "plannedDepartureTrack": "8", + "plannedArrivalTrack": "8", + "actualArrivalTrack": "8", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "MEDIUM", + "punctuality": 58.3, + "crossPlatformTransfer": true, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#505945#TA#0#DA#150925#1S#1101167#1T#1550#LS#1101102#LT#1833#PU#784#RT#3#CA#IC#ZE#3661#ZB#IC 3661 #PC#1#FR#1101167#FT#1550#TO#1101102#TT#1833#&train=3661&datetime=2025-09-15T17:41:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 29, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "29 min.", + "accessibilityValue": "29 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 2, + "distanceInMeters": 41871 + }, + { + "idx": "2", + "name": "ICD 1871", + "travelType": "PUBLIC_TRANSIT", + "direction": "Amersfoort Schothorst", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#442492#TA#1#DA#150925#1S#1100942#1T#1814#LS#1100687#LT#1954#PU#784#RT#1#CA#ICD#ZE#1871#ZB#ICD 1871#PC#1#FR#1100942#FT#1814#TO#1100687#TT#1954#", + "origin": { + "name": "Breda", + "lng": 4.78000020980835, + "lat": 51.5955543518066, + "countryCode": "NL", + "uicCode": "8400131", + "uicCdCode": "118400131", + "stationCode": "BD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T10:14:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T10:14:00+0200", + "plannedTrack": "7", + "actualTrack": "7", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "Rotterdam Centraal", + "lng": 4.46888875961304, + "lat": 51.9249992370605, + "countryCode": "NL", + "uicCode": "8400530", + "uicCdCode": "118400530", + "stationCode": "RTD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T10:37:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T10:37:00+0200", + "plannedTrack": "12", + "actualTrack": "12", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "1871", + "categoryCode": "ICD", + "shortCategoryName": "ICD", + "longCategoryName": "Intercity direct", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity direct", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity direct", + "shortValue": "NS Intercity direct", + "accessibilityValue": "NS Intercity direct", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Amersfoort Schothorst via Schiphol Airport", + "shortValue": "richting Amersfoort Schothorst via Schiphol Airport", + "accessibilityValue": "richting Amersfoort Schothorst via Schiphol Airport", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "Geen tussenstops", + "shortValue": "Geen tussenstops", + "accessibilityValue": "Geen tussenstops", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "transferMessages": [ + { + "message": "Overstap op zelfde perron", + "accessibilityMessage": "Overstap op zelfde perron", + "type": "CROSS_PLATFORM", + "messageNesProperties": { + "color": "text-default", + "type": "informative" + } + } + ], + "stops": [ + { + "uicCode": "8400131", + "uicCdCode": "118400131", + "name": "Breda", + "lat": 51.5955543518066, + "lng": 4.78000020980835, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-16T10:14:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-16T10:14:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "7", + "plannedDepartureTrack": "7", + "plannedArrivalTrack": "7", + "actualArrivalTrack": "7", + "departureDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400530", + "uicCdCode": "118400530", + "name": "Rotterdam Centraal", + "lat": 51.9249992370605, + "lng": 4.46888875961304, + "countryCode": "NL", + "notes": [], + "routeIdx": 6, + "plannedArrivalDateTime": "2025-09-16T10:37:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-16T10:37:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "12", + "plannedDepartureTrack": "12", + "plannedArrivalTrack": "12", + "actualArrivalTrack": "12", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 16, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#442492#TA#1#DA#150925#1S#1100942#1T#1814#LS#1100687#LT#1954#PU#784#RT#1#CA#ICD#ZE#1871#ZB#ICD 1871#PC#1#FR#1100942#FT#1814#TO#1100687#TT#1954#&train=1871&datetime=2025-09-16T10:14:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 23, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "23 min.", + "accessibilityValue": "23 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "recognizableDestination": "Schiphol Airport", + "distanceInMeters": 44166 + } + ], + "checksum": "dc23b827_3", + "crowdForecast": "MEDIUM", + "punctuality": 58.3, + "optimal": true, + "fares": [], + "fareLegs": [ + { + "origin": { + "name": "Amsterdam Centraal", + "lng": 4.90027761459351, + "lat": 52.3788871765137, + "countryCode": "NL", + "uicCode": "8400058", + "uicCdCode": "118400058", + "stationCode": "ASD", + "type": "STATION" + }, + "destination": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION" + }, + "operator": "NS", + "productTypes": ["TRAIN"], + "fares": [ + { + "priceInCents": 1910, + "priceInCentsExcludingSupplement": 1910, + "supplementInCents": 0, + "buyableTicketSupplementPriceInCents": 0, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + } + ], + "travelDate": "2025-09-15" + }, + { + "origin": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION" + }, + "destination": { + "name": "Rotterdam Centraal", + "lng": 4.46888875961304, + "lat": 51.9249992370605, + "countryCode": "NL", + "uicCode": "8400530", + "uicCdCode": "118400530", + "stationCode": "RTD", + "type": "STATION" + }, + "operator": "NS", + "productTypes": ["TRAIN"], + "fares": [ + { + "priceInCents": 2010, + "priceInCentsExcludingSupplement": 2010, + "supplementInCents": 0, + "buyableTicketSupplementPriceInCents": 0, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + } + ], + "travelDate": "2025-09-15" + } + ], + "productFare": { + "priceInCents": 3920, + "priceInCentsExcludingSupplement": 3920, + "buyableTicketPriceInCents": 3920, + "buyableTicketPriceInCentsExcludingSupplement": 3920, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + }, + "fareOptions": { + "isInternationalBookable": false, + "isInternational": false, + "isEticketBuyable": false, + "isPossibleWithOvChipkaart": false, + "isTotalPriceUnknown": false, + "reasonEticketNotBuyable": { + "reason": "VIA_STATION_REQUESTED", + "description": "Je kunt voor deze reis geen kaartje kopen, omdat je je reis via een extra station hebt gepland. Uiteraard kun je voor deze reis betalen met het saldo op je OV-chipkaart." + } + }, + "nsiLink": { + "url": "https://www.nsinternational.com/nl/treintickets-v3/#/search/ASD/RTD/20250915/1634/1837?stationType=domestic&cookieConsent=false", + "showInternationalBanner": false + }, + "type": "NS", + "shareUrl": { + "uri": "https://www.ns.nl/rpx?ctx=arnu%7CfromStation%3D8400058%7CrequestedFromStation%3D8400058%7CtoStation%3D8400530%7CrequestedToStation%3D8400530%7CviaStation%3D8400319%7CplannedFromTime%3D2025-09-15T16%3A34%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T18%3A37%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D1180343963" + }, + "realtime": true, + "registerJourney": { + "url": "https://treinwijzer.ns.nl/idp/login?ctxRecon=arnu%7CfromStation%3D8400058%7CrequestedFromStation%3D8400058%7CtoStation%3D8400530%7CrequestedToStation%3D8400530%7CviaStation%3D8400319%7CplannedFromTime%3D2025-09-15T16%3A34%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T18%3A37%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D1180343963&originUicCode=8400058&destinationUicCode=8400530&dateTime=2025-09-15T16%3A28%3A00.051873%2B02%3A00&searchForArrival=false&viaUicCode=8400319&excludeHighSpeedTrains=false&localTrainsOnly=false&searchForAccessibleTrip=false&lang=nl&travelAssistance=false", + "searchUrl": "https://treinwijzer.ns.nl/idp/login?search=true&originUicCode=8400058&destinationUicCode=8400530&dateTime=2025-09-15T16%3A28%3A00.051873%2B02%3A00&searchForArrival=false&viaUicCode=8400319&excludeHighSpeedTrains=false&localTrainsOnly=false&searchForAccessibleTrip=false&lang=nl&travelAssistance=false", + "status": "REGISTRATION_POSSIBLE", + "bicycleReservationRequired": false + }, + "modalityListItems": [ + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "4", + "accessibilityName": "Intercity" + }, + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "7", + "accessibilityName": "Intercity" + }, + { + "name": "Intercity direct", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "7", + "accessibilityName": "Intercity direct" + } + ] + }, + { + "idx": 2, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-16T08:34:00+02:00|plannedArrivalTime=2025-09-16T10:45:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1596512355", + "ctxRecon": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-16T08:34:00+02:00|plannedArrivalTime=2025-09-16T10:45:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1596512355", + "sourceCtxRecon": "\u00b6HKI\u00b6T$A=1@O=Amsterdam Centraal@L=1100836@a=128@$A=1@O='s-Hertogenbosch@L=1100870@a=128@$202509161634$202509161731$IC 2761 $$1$$$$$$\u00a7W$A=1@O='s-Hertogenbosch@L=1100870@a=128@$A=1@O='s-Hertogenbosch@L=1101751@a=128@$202509161731$202509161733$$$1$$$$$$\u00a7T$A=1@O='s-Hertogenbosch@L=1101751@a=128@$A=1@O=Breda@L=1101034@a=128@$202509161741$202509161810$IC 3661 $$3$$$$$$\u00a7W$A=1@O=Breda@L=1101034@a=128@$A=1@O=Breda@L=1100942@a=128@$202509161810$202509161812$$$1$$$$$$\u00a7T$A=1@O=Breda@L=1100942@a=128@$A=1@O=Rotterdam Centraal@L=1100668@a=128@$202509161823$202509161845$IC 1162 $$1$$$$$$\u00b6KC\u00b6#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#\u00b6KCC\u00b6#VE#0#ERG#45317#HIN#390#ECK#13954|13954|14077|14085|0|0|485|13938|3|0|8|0|0|-2147483648#\u00b6KRCC\u00b6#VE#1#MRTF#", + "plannedDurationInMinutes": 131, + "actualDurationInMinutes": 130, + "transfers": 2, + "status": "NORMAL", + "messages": [], + "legs": [ + { + "idx": "0", + "name": "IC 2761", + "travelType": "PUBLIC_TRANSIT", + "direction": "Maastricht", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1088#TA#0#DA#150925#1S#1101009#1T#1557#LS#1101011#LT#1903#PU#784#RT#1#CA#IC#ZE#2761#ZB#IC 2761 #PC#1#FR#1101009#FT#1557#TO#1101011#TT#1903#", + "origin": { + "name": "Amsterdam Centraal", + "lng": 4.90027761459351, + "lat": 52.3788871765137, + "countryCode": "NL", + "uicCode": "8400058", + "uicCdCode": "118400058", + "stationCode": "ASD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T08:34:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T08:35:00+0200", + "plannedTrack": "4", + "actualTrack": "4", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T17:31:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:31:00+0200", + "plannedTrack": "6", + "actualTrack": "6", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "2761", + "categoryCode": "IC", + "shortCategoryName": "IC", + "longCategoryName": "Intercity", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity", + "shortValue": "NS Intercity", + "accessibilityValue": "NS Intercity", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Maastricht", + "shortValue": "richting Maastricht", + "accessibilityValue": "richting Maastricht", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "2 tussenstops", + "shortValue": "2 tussenstops", + "accessibilityValue": "2 tussenstops", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "stops": [ + { + "uicCode": "8400058", + "uicCdCode": "118400058", + "name": "Amsterdam Centraal", + "lat": 52.3788871765137, + "lng": 4.90027761459351, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-16T08:34:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-16T08:35:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 60, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400057", + "uicCdCode": "118400057", + "name": "Amsterdam Amstel", + "lat": 52.3466682434082, + "lng": 4.91777801513672, + "countryCode": "NL", + "notes": [], + "routeIdx": 2, + "plannedDepartureDateTime": "2025-09-15T16:42:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16:43:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T16:42:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T16:43:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 60, + "arrivalDelayInSeconds": 60, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400621", + "uicCdCode": "118400621", + "name": "Utrecht Centraal", + "lat": 52.0888900756836, + "lng": 5.11027765274048, + "countryCode": "NL", + "notes": [], + "routeIdx": 10, + "plannedDepartureDateTime": "2025-09-15T17:03:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:03:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:00:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:00:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "15", + "plannedDepartureTrack": "15", + "plannedArrivalTrack": "15", + "actualArrivalTrack": "15", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400319", + "uicCdCode": "118400319", + "name": "'s-Hertogenbosch", + "lat": 51.69048, + "lng": 5.29362, + "countryCode": "NL", + "notes": [], + "routeIdx": 18, + "plannedArrivalDateTime": "2025-09-15T17:31:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:31:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "6", + "plannedDepartureTrack": "6", + "plannedArrivalTrack": "6", + "actualArrivalTrack": "6", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "MEDIUM", + "bicycleSpotCount": 6, + "crossPlatformTransfer": true, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1088#TA#0#DA#150925#1S#1101009#1T#1557#LS#1101011#LT#1903#PU#784#RT#1#CA#IC#ZE#2761#ZB#IC 2761 #PC#1#FR#1101009#FT#1557#TO#1101011#TT#1903#&train=2761&datetime=2025-09-16T08:34:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 57, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "56 min.", + "accessibilityValue": "56 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 2, + "distanceInMeters": 84795 + }, + { + "idx": "1", + "name": "IC 3661", + "travelType": "PUBLIC_TRANSIT", + "direction": "Roosendaal", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#505945#TA#0#DA#150925#1S#1101167#1T#1550#LS#1101102#LT#1833#PU#784#RT#3#CA#IC#ZE#3661#ZB#IC 3661 #PC#1#FR#1101167#FT#1550#TO#1101102#TT#1833#", + "origin": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T17:41:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:41:00+0200", + "plannedTrack": "7", + "actualTrack": "7", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "Breda", + "lng": 4.78000020980835, + "lat": 51.5955543518066, + "countryCode": "NL", + "uicCode": "8400131", + "uicCdCode": "118400131", + "stationCode": "BD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T10:10:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T10:10:00+0200", + "plannedTrack": "8", + "actualTrack": "8", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3661", + "categoryCode": "IC", + "shortCategoryName": "IC", + "longCategoryName": "Intercity", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity", + "shortValue": "NS Intercity", + "accessibilityValue": "NS Intercity", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Roosendaal", + "shortValue": "richting Roosendaal", + "accessibilityValue": "richting Roosendaal", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "1 tussenstop", + "shortValue": "1 tussenstop", + "accessibilityValue": "1 tussenstop", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "transferMessages": [ + { + "message": "Overstap op zelfde perron", + "accessibilityMessage": "Overstap op zelfde perron", + "type": "CROSS_PLATFORM", + "messageNesProperties": { + "color": "text-default", + "type": "informative" + } + } + ], + "stops": [ + { + "uicCode": "8400319", + "uicCdCode": "118400319", + "name": "'s-Hertogenbosch", + "lat": 51.69048, + "lng": 5.29362, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-15T17:41:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:41:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "7", + "plannedDepartureTrack": "7", + "plannedArrivalTrack": "7", + "actualArrivalTrack": "7", + "departureDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400597", + "uicCdCode": "118400597", + "name": "Tilburg", + "lat": 51.5605545043945, + "lng": 5.08361101150513, + "countryCode": "NL", + "notes": [], + "routeIdx": 1, + "plannedDepartureDateTime": "2025-09-15T17:58:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:58:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:56:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:56:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "3", + "plannedDepartureTrack": "3", + "plannedArrivalTrack": "3", + "actualArrivalTrack": "3", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400131", + "uicCdCode": "118400131", + "name": "Breda", + "lat": 51.5955543518066, + "lng": 4.78000020980835, + "countryCode": "NL", + "notes": [], + "routeIdx": 5, + "plannedArrivalDateTime": "2025-09-16T10:10:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-16T10:10:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "8", + "plannedDepartureTrack": "8", + "plannedArrivalTrack": "8", + "actualArrivalTrack": "8", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "MEDIUM", + "punctuality": 58.3, + "crossPlatformTransfer": true, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#505945#TA#0#DA#150925#1S#1101167#1T#1550#LS#1101102#LT#1833#PU#784#RT#3#CA#IC#ZE#3661#ZB#IC 3661 #PC#1#FR#1101167#FT#1550#TO#1101102#TT#1833#&train=3661&datetime=2025-09-15T17:41:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 29, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "29 min.", + "accessibilityValue": "29 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 2, + "distanceInMeters": 41871 + }, + { + "idx": "2", + "name": "IC 1162", + "travelType": "PUBLIC_TRANSIT", + "direction": "Den Haag Centraal", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#51#TA#9#DA#150925#1S#1100921#1T#1743#LS#1101078#LT#1911#PU#784#RT#1#CA#IC#ZE#1162#ZB#IC 1162 #PC#1#FR#1100921#FT#1743#TO#1101078#TT#1911#", + "origin": { + "name": "Breda", + "lng": 4.78000020980835, + "lat": 51.5955543518066, + "countryCode": "NL", + "uicCode": "8400131", + "uicCdCode": "118400131", + "stationCode": "BD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T10:23:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T10:23:00+0200", + "plannedTrack": "7", + "actualTrack": "7", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "Rotterdam Centraal", + "lng": 4.46888875961304, + "lat": 51.9249992370605, + "countryCode": "NL", + "uicCode": "8400530", + "uicCdCode": "118400530", + "stationCode": "RTD", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-16T10:45:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-16T10:45:00+0200", + "plannedTrack": "13", + "actualTrack": "13", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "1162", + "categoryCode": "IC", + "shortCategoryName": "IC", + "longCategoryName": "Intercity", + "operatorCode": "NS", + "operatorName": "NS", + "operatorAdministrativeCode": 100, + "type": "TRAIN", + "displayName": "NS Intercity", + "nameNesProperties": { + "color": "text-body" + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "notes": [ + [ + { + "value": "NS Intercity", + "shortValue": "NS Intercity", + "accessibilityValue": "NS Intercity", + "key": "PRODUCT_NAME", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "richting Den Haag Centraal", + "shortValue": "richting Den Haag Centraal", + "accessibilityValue": "richting Den Haag Centraal", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "Geen tussenstops", + "shortValue": "Geen tussenstops", + "accessibilityValue": "Geen tussenstops", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "transferMessages": [ + { + "message": "Overstap op zelfde perron", + "accessibilityMessage": "Overstap op zelfde perron", + "type": "CROSS_PLATFORM", + "messageNesProperties": { + "color": "text-default", + "type": "informative" + } + } + ], + "stops": [ + { + "uicCode": "8400131", + "uicCdCode": "118400131", + "name": "Breda", + "lat": 51.5955543518066, + "lng": 4.78000020980835, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-16T10:23:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-16T10:23:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "7", + "plannedDepartureTrack": "7", + "plannedArrivalTrack": "7", + "actualArrivalTrack": "7", + "departureDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400530", + "uicCdCode": "118400530", + "name": "Rotterdam Centraal", + "lat": 51.9249992370605, + "lng": 4.46888875961304, + "countryCode": "NL", + "notes": [], + "routeIdx": 6, + "plannedArrivalDateTime": "2025-09-16T10:45:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-16T10:45:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "13", + "plannedDepartureTrack": "13", + "plannedArrivalTrack": "13", + "actualArrivalTrack": "13", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 16, + "punctuality": 81.8, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#51#TA#9#DA#150925#1S#1100921#1T#1743#LS#1101078#LT#1911#PU#784#RT#1#CA#IC#ZE#1162#ZB#IC 1162 #PC#1#FR#1100921#FT#1743#TO#1101078#TT#1911#&train=1162&datetime=2025-09-16T10:23:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 22, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "22 min.", + "accessibilityValue": "22 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "distanceInMeters": 44166 + } + ], + "checksum": "fe950328_3", + "crowdForecast": "MEDIUM", + "punctuality": 58.3, + "optimal": false, + "fares": [], + "fareLegs": [ + { + "origin": { + "name": "Amsterdam Centraal", + "lng": 4.90027761459351, + "lat": 52.3788871765137, + "countryCode": "NL", + "uicCode": "8400058", + "uicCdCode": "118400058", + "stationCode": "ASD", + "type": "STATION" + }, + "destination": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION" + }, + "operator": "NS", + "productTypes": ["TRAIN"], + "fares": [ + { + "priceInCents": 1910, + "priceInCentsExcludingSupplement": 1910, + "supplementInCents": 0, + "buyableTicketSupplementPriceInCents": 0, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + } + ], + "travelDate": "2025-09-15" + }, + { + "origin": { + "name": "'s-Hertogenbosch", + "lng": 5.29362, + "lat": 51.69048, + "countryCode": "NL", + "uicCode": "8400319", + "uicCdCode": "118400319", + "stationCode": "HT", + "type": "STATION" + }, + "destination": { + "name": "Rotterdam Centraal", + "lng": 4.46888875961304, + "lat": 51.9249992370605, + "countryCode": "NL", + "uicCode": "8400530", + "uicCdCode": "118400530", + "stationCode": "RTD", + "type": "STATION" + }, + "operator": "NS", + "productTypes": ["TRAIN"], + "fares": [ + { + "priceInCents": 2010, + "priceInCentsExcludingSupplement": 2010, + "supplementInCents": 0, + "buyableTicketSupplementPriceInCents": 0, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + } + ], + "travelDate": "2025-09-15" + } + ], + "productFare": { + "priceInCents": 3920, + "priceInCentsExcludingSupplement": 3920, + "buyableTicketPriceInCents": 3920, + "buyableTicketPriceInCentsExcludingSupplement": 3920, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + }, + "fareOptions": { + "isInternationalBookable": false, + "isInternational": false, + "isEticketBuyable": false, + "isPossibleWithOvChipkaart": false, + "isTotalPriceUnknown": false, + "reasonEticketNotBuyable": { + "reason": "VIA_STATION_REQUESTED", + "description": "Je kunt voor deze reis geen kaartje kopen, omdat je je reis via een extra station hebt gepland. Uiteraard kun je voor deze reis betalen met het saldo op je OV-chipkaart." + } + }, + "nsiLink": { + "url": "https://www.nsinternational.com/nl/treintickets-v3/#/search/ASD/RTD/20250915/1634/1845?stationType=domestic&cookieConsent=false", + "showInternationalBanner": false + }, + "type": "NS", + "shareUrl": { + "uri": "https://www.ns.nl/rpx?ctx=arnu%7CfromStation%3D8400058%7CrequestedFromStation%3D8400058%7CtoStation%3D8400530%7CrequestedToStation%3D8400530%7CviaStation%3D8400319%7CplannedFromTime%3D2025-09-15T16%3A34%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T18%3A45%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D1596512355" + }, + "realtime": true, + "registerJourney": { + "url": "https://treinwijzer.ns.nl/idp/login?ctxRecon=arnu%7CfromStation%3D8400058%7CrequestedFromStation%3D8400058%7CtoStation%3D8400530%7CrequestedToStation%3D8400530%7CviaStation%3D8400319%7CplannedFromTime%3D2025-09-15T16%3A34%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T18%3A45%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D1596512355&originUicCode=8400058&destinationUicCode=8400530&dateTime=2025-09-15T16%3A28%3A00.051873%2B02%3A00&searchForArrival=false&viaUicCode=8400319&excludeHighSpeedTrains=false&localTrainsOnly=false&searchForAccessibleTrip=false&lang=nl&travelAssistance=false", + "searchUrl": "https://treinwijzer.ns.nl/idp/login?search=true&originUicCode=8400058&destinationUicCode=8400530&dateTime=2025-09-15T16%3A28%3A00.051873%2B02%3A00&searchForArrival=false&viaUicCode=8400319&excludeHighSpeedTrains=false&localTrainsOnly=false&searchForAccessibleTrip=false&lang=nl&travelAssistance=false", + "status": "REGISTRATION_POSSIBLE", + "bicycleReservationRequired": false + }, + "modalityListItems": [ + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "4", + "accessibilityName": "Intercity" + }, + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "7", + "accessibilityName": "Intercity" + }, + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "7", + "accessibilityName": "Intercity" + } + ] + } + ] +} diff --git a/tests/components/nederlandse_spoorwegen/test_sensor.py b/tests/components/nederlandse_spoorwegen/test_sensor.py index 35542a72f72..2cf48d67d5e 100644 --- a/tests/components/nederlandse_spoorwegen/test_sensor.py +++ b/tests/components/nederlandse_spoorwegen/test_sensor.py @@ -1,7 +1,9 @@ """Test the Nederlandse Spoorwegen sensor.""" from collections.abc import Generator +from datetime import date, datetime from unittest.mock import AsyncMock, patch +import zoneinfo import pytest from requests.exceptions import ConnectionError as RequestsConnectionError @@ -208,3 +210,172 @@ async def test_sensor_with_custom_time_parsing( route_name.lower() in friendly_name or route_name.replace(" ", "_").lower() in state.entity_id ) + + +@pytest.mark.freeze_time("2025-09-15 14:30:00+00:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_with_time_filtering( + hass: HomeAssistant, + mock_nsapi: AsyncMock, +) -> None: + """Test that the time-based window filter correctly filters trips. + + This test verifies that: + 1. Trips BEFORE the configured time are filtered out + 2. Trips AT or AFTER the configured time are included + 3. The filtering is based on time-only (ignoring date) + """ + # Create a config entry with a route that has time set to 16:00 + # Test frozen at: 2025-09-15 14:30 UTC = 16:30 Amsterdam time + # The fixture includes trips at the following times: + # 16:24/16:25 (trip 0) - FILTERED OUT (departed before 16:30 now) + # 16:34/16:35 (trip 1) - INCLUDED (>= 16:00 configured time AND > 16:30 now) + # With time=16:00, only future trips at or after 16:00 are included + config_entry = MockConfigEntry( + title=INTEGRATION_TITLE, + data={CONF_API_KEY: API_KEY}, + domain=DOMAIN, + subentries_data=[ + ConfigSubentryDataWithId( + data={ + CONF_NAME: "Afternoon commute", + CONF_FROM: "Ams", + CONF_TO: "Rot", + CONF_VIA: "Ht", + CONF_TIME: "16:00", + }, + subentry_type=SUBENTRY_TYPE_ROUTE, + title="Afternoon Route", + unique_id=None, + subentry_id="test_route_time_filter", + ), + ], + ) + + await setup_integration(hass, config_entry) + await hass.async_block_till_done() + + # Should create sensors for the route + sensor_states = hass.states.async_all("sensor") + assert len(sensor_states) == 13 + + # Find the actual departure time sensor and next departure sensor + actual_departure_sensor = hass.states.get("sensor.afternoon_commute_departure") + next_departure_sensor = hass.states.get("sensor.afternoon_commute_next_departure") + + assert actual_departure_sensor is not None, "Actual departure sensor not found" + assert actual_departure_sensor.state != STATE_UNKNOWN + + # The sensor state is a UTC timestamp, convert it to Amsterdam time + ams_tz = zoneinfo.ZoneInfo("Europe/Amsterdam") + + departure_dt = datetime.fromisoformat(actual_departure_sensor.state) + departure_local = departure_dt.astimezone(ams_tz) + + hour = departure_local.hour + minute = departure_local.minute + # Verify first trip: is NOT before 16:00 (i.e., filtered trips are excluded) + assert hour >= 16, ( + f"Expected first trip at or after 16:00 Amsterdam time, but got {hour}:{minute:02d}. " + "This means trips before the configured time were NOT filtered out by the time window filter." + ) + + # Verify next trip also passes the filter + assert next_departure_sensor is not None, "Next departure sensor not found" + next_departure_dt = datetime.fromisoformat(next_departure_sensor.state) + next_departure_local = next_departure_dt.astimezone(ams_tz) + + next_hour = next_departure_local.hour + next_minute = next_departure_local.minute + + # Verify next trip is also at or after 16:00 + assert next_hour >= 16, ( + f"Expected next trip at or after 16:00 Amsterdam time, but got {next_hour}:{next_minute:02d}. " + "This means the window filter is not applied consistently to all trips." + ) + + # Verify next trip is after the first trip + assert (next_hour, next_minute) > (hour, minute), ( + f"Expected next trip ({next_hour}:{next_minute:02d}) to be after first trip ({hour}:{minute:02d})" + ) + + +@pytest.mark.freeze_time("2025-09-15 14:30:00+00:00") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor_with_time_filtering_next_day( + hass: HomeAssistant, + mock_tomorrow_trips_nsapi: AsyncMock, +) -> None: + """Test that time filtering automatically rolls over to next day when time is in past. + + This test verifies the day boundary logic: + 1. When configured time is >1 hour in the past, coordinator queries tomorrow's trips + 2. The API is called with tomorrow's date + configured time + 3. This ensures users get their morning commute trips even when configured in evening + + Example: It's 16:30 (4:30 PM), user configured 08:00 (8:00 AM) for morning commute. + Instead of showing no trips (since 08:00 already passed today), we show tomorrow's 08:00 trips. + """ + # Current time: 16:30 Amsterdam (14:30 UTC frozen) + # Configured time: 08:00 (8.5 hours in the past, >1 hour threshold) + # Expected behavior: Query tomorrow (2025-09-16) at 08:00 + config_entry = MockConfigEntry( + title=INTEGRATION_TITLE, + data={CONF_API_KEY: API_KEY}, + domain=DOMAIN, + subentries_data=[ + ConfigSubentryDataWithId( + data={ + CONF_NAME: "Morning commute", + CONF_FROM: "Ams", + CONF_TO: "Rot", + CONF_VIA: "Ht", + CONF_TIME: "08:00", + }, + subentry_type=SUBENTRY_TYPE_ROUTE, + title="Morning Route", + unique_id=None, + subentry_id="test_route_morning", + ), + ], + ) + + await setup_integration(hass, config_entry) + await hass.async_block_till_done() + + # Should create sensors for the route + sensor_states = hass.states.async_all("sensor") + assert len(sensor_states) == 13 + + # Find the actual departure sensor + actual_departure_sensor = hass.states.get("sensor.morning_commute_departure") + + assert actual_departure_sensor is not None, "Actual departure sensor not found" + + # The sensor should have a valid trip + assert actual_departure_sensor.state != STATE_UNKNOWN, ( + "Expected to have trips from tomorrow when configured time is in the past" + ) + + # Verify the first trip is tomorrow morning at or after 08:00 + # The fixture has trips at 08:24, 08:34 on 2025-09-16 (tomorrow) + departure_dt = datetime.fromisoformat(actual_departure_sensor.state) + ams_tz = zoneinfo.ZoneInfo("Europe/Amsterdam") + departure_local = departure_dt.astimezone(ams_tz) + + departure_hour = departure_local.hour + departure_minute = departure_local.minute + departure_date = departure_local.date() + + # Verify trip is at or after 08:00 morning time + assert 8 <= departure_hour < 12, ( + f"Expected morning trip (08:00-11:59) but got {departure_hour}:{departure_minute:02d}. " + "This means the rollover to tomorrow logic is not working correctly." + ) + + # Verify trip is from tomorrow (2025-09-16) + expected_date = date(2025, 9, 16) + assert departure_date == expected_date, ( + f"Expected trip from tomorrow (2025-09-16) but got {departure_date}. " + "The coordinator should query tomorrow's trips when configured time is >1 hour in the past." + )