diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 9a1ace9994ae..baa954cdbdac 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -6,6 +6,7 @@ from datetime import datetime import logging from typing import Any +from ns_api import Trip import voluptuous as vol from homeassistant.components.sensor import ( @@ -38,6 +39,33 @@ from .const import ( ) from .coordinator import NSConfigEntry, NSDataUpdateCoordinator + +def _get_departure_time(trip: Trip | None) -> datetime | None: + """Get next departure time from trip data.""" + return trip.departure_time_actual or trip.departure_time_planned if trip else None + + +def _get_time_str(time: datetime | None) -> str | None: + """Get time as string.""" + return time.strftime("%H:%M") if time else None + + +def _get_route(trip: Trip | None) -> list[str]: + """Get the route as a list of station names from trip data.""" + if not trip or not (trip_parts := trip.trip_parts): + return [] + route = [] + if departure := trip.departure: + route.append(departure) + route.extend(part.destination for part in trip_parts) + return route + + +def _get_delay(planned: datetime | None, actual: datetime | None) -> bool: + """Return True if delay is present, False otherwise.""" + return bool(planned and actual and planned != actual) + + _LOGGER = logging.getLogger(__name__) ROUTE_SCHEMA = vol.Schema( @@ -163,94 +191,38 @@ class NSDepartureSensor(CoordinatorEntity[NSDataUpdateCoordinator], SensorEntity return None first_trip = route_data.first_trip - if first_trip.departure_time_actual: - return first_trip.departure_time_actual - return first_trip.departure_time_planned + return _get_departure_time(first_trip) @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" - route_data = self.coordinator.data - if not route_data: - return None - - first_trip = route_data.first_trip - next_trip = route_data.next_trip + first_trip = self.coordinator.data.first_trip + next_trip = self.coordinator.data.next_trip if not first_trip: return None - route = [] - if first_trip.trip_parts: - route = [first_trip.departure] - route.extend(k.destination for k in first_trip.trip_parts) - - # Static attributes - attributes = { + return { "going": first_trip.going, - "departure_time_planned": None, - "departure_time_actual": None, - "departure_delay": False, + "departure_time_planned": _get_time_str(first_trip.departure_time_planned), + "departure_time_actual": _get_time_str(first_trip.departure_time_actual), + "departure_delay": _get_delay( + first_trip.departure_time_planned, + first_trip.departure_time_actual, + ), "departure_platform_planned": first_trip.departure_platform_planned, "departure_platform_actual": first_trip.departure_platform_actual, - "arrival_time_planned": None, - "arrival_time_actual": None, - "arrival_delay": False, + "arrival_time_planned": _get_time_str(first_trip.arrival_time_planned), + "arrival_time_actual": _get_time_str(first_trip.arrival_time_actual), + "arrival_delay": _get_delay( + first_trip.arrival_time_planned, + first_trip.arrival_time_actual, + ), "arrival_platform_planned": first_trip.arrival_platform_planned, "arrival_platform_actual": first_trip.arrival_platform_actual, - "next": None, + "next": _get_time_str(_get_departure_time(next_trip)), "status": first_trip.status.lower() if first_trip.status else None, "transfers": first_trip.nr_transfers, - "route": route, + "route": _get_route(first_trip), "remarks": None, } - - # Planned departure attributes - if first_trip.departure_time_planned is not None: - attributes["departure_time_planned"] = ( - first_trip.departure_time_planned.strftime("%H:%M") - ) - - # Actual departure attributes - if first_trip.departure_time_actual is not None: - attributes["departure_time_actual"] = ( - first_trip.departure_time_actual.strftime("%H:%M") - ) - - # Delay departure attributes - if ( - attributes["departure_time_planned"] - and attributes["departure_time_actual"] - and attributes["departure_time_planned"] - != attributes["departure_time_actual"] - ): - attributes["departure_delay"] = True - - # Planned arrival attributes - if first_trip.arrival_time_planned is not None: - attributes["arrival_time_planned"] = ( - first_trip.arrival_time_planned.strftime("%H:%M") - ) - - # Actual arrival attributes - if first_trip.arrival_time_actual is not None: - attributes["arrival_time_actual"] = first_trip.arrival_time_actual.strftime( - "%H:%M" - ) - - # Delay arrival attributes - if ( - attributes["arrival_time_planned"] - and attributes["arrival_time_actual"] - and attributes["arrival_time_planned"] != attributes["arrival_time_actual"] - ): - attributes["arrival_delay"] = True - - # Next trip attributes - if next_trip: - if next_trip.departure_time_actual is not None: - attributes["next"] = next_trip.departure_time_actual.strftime("%H:%M") - elif next_trip.departure_time_planned is not None: - attributes["next"] = next_trip.departure_time_planned.strftime("%H:%M") - - return attributes diff --git a/tests/components/nederlandse_spoorwegen/conftest.py b/tests/components/nederlandse_spoorwegen/conftest.py index 8729a0cd5b34..100a72056222 100644 --- a/tests/components/nederlandse_spoorwegen/conftest.py +++ b/tests/components/nederlandse_spoorwegen/conftest.py @@ -56,6 +56,21 @@ def mock_nsapi() -> Generator[AsyncMock]: yield client +@pytest.fixture +def mock_single_trip_nsapi(mock_nsapi: AsyncMock) -> Generator[AsyncMock]: + """Override async_setup_entry.""" + trips_data = load_json_object_fixture("trip_single.json", DOMAIN) + mock_nsapi.get_trips.return_value = [Trip(trip) for trip in trips_data["trips"]] + return mock_nsapi + + +@pytest.fixture +def mock_no_trips_nsapi(mock_nsapi: AsyncMock) -> Generator[AsyncMock]: + """Override async_setup_entry.""" + mock_nsapi.get_trips.return_value = [] + return mock_nsapi + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Mock config entry.""" diff --git a/tests/components/nederlandse_spoorwegen/fixtures/trip_single.json b/tests/components/nederlandse_spoorwegen/fixtures/trip_single.json new file mode 100644 index 000000000000..8846dc8172cd --- /dev/null +++ b/tests/components/nederlandse_spoorwegen/fixtures/trip_single.json @@ -0,0 +1,856 @@ +{ + "source": "HARP", + "trips": [ + { + "idx": 2, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T16:34:00+02:00|plannedArrivalTime=2025-09-15T18: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-15T16:34:00+02:00|plannedArrivalTime=2025-09-15T18:45:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1596512355", + "sourceCtxRecon": "¶HKI¶T$A=1@O=Amsterdam Centraal@L=1100836@a=128@$A=1@O='s-Hertogenbosch@L=1100870@a=128@$202509151634$202509151731$IC 2761 $$1$$$$$$§W$A=1@O='s-Hertogenbosch@L=1100870@a=128@$A=1@O='s-Hertogenbosch@L=1101751@a=128@$202509151731$202509151733$$$1$$$$$$§T$A=1@O='s-Hertogenbosch@L=1101751@a=128@$A=1@O=Breda@L=1101034@a=128@$202509151741$202509151810$IC 3661 $$3$$$$$$§W$A=1@O=Breda@L=1101034@a=128@$A=1@O=Breda@L=1100942@a=128@$202509151810$202509151812$$$1$$$$$$§T$A=1@O=Breda@L=1100942@a=128@$A=1@O=Rotterdam Centraal@L=1100668@a=128@$202509151823$202509151845$IC 1162 $$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#¶KCC¶#VE#0#ERG#45317#HIN#390#ECK#13954|13954|14077|14085|0|0|485|13938|3|0|8|0|0|-2147483648#¶KRCC¶#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-15T16:34:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T16: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-15T16:34:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16: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-15T16: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-15T18:10:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18: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-15T18:10:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18: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-15T18:23:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18: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-15T18:45:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18: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-15T18:23:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18: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-15T18:45:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18: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-15T18: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" + } + ] + } + ], + "scrollRequestBackwardContext": "3|OB|MTµ14µ13954µ13944µ14077µ14080µ0µ0µ485µ13938µ1µ0µ8µ0µ0µ-2147483648µ1µ2|PDHµ28839c70675e70c3d48018993723bca2|RDµ15092025|RTµ161800|USµ0|RSµINIT", + "scrollRequestForwardContext": "3|OF|MTµ14µ13985µ13985µ14107µ14115µ0µ0µ485µ13955µ8µ0µ8µ0µ0µ-2147483648µ1µ2|PDHµ28839c70675e70c3d48018993723bca2|RDµ15092025|RTµ161800|USµ0|RSµINIT" +} diff --git a/tests/components/nederlandse_spoorwegen/snapshots/test_sensor.ambr b/tests/components/nederlandse_spoorwegen/snapshots/test_sensor.ambr index 565970b953f6..998e63811614 100644 --- a/tests/components/nederlandse_spoorwegen/snapshots/test_sensor.ambr +++ b/tests/components/nederlandse_spoorwegen/snapshots/test_sensor.ambr @@ -1,4 +1,106 @@ # serializer version: 1 +# name: test_no_trips_sensor[sensor.to_home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.to_home', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:train', + 'original_name': 'To home', + 'platform': 'nederlandse_spoorwegen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01K721DZPMEN39R5DK0ATBMSY9-actual_departure', + 'unit_of_measurement': None, + }) +# --- +# name: test_no_trips_sensor[sensor.to_home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by NS', + 'device_class': 'timestamp', + 'friendly_name': 'To home', + 'icon': 'mdi:train', + }), + 'context': , + 'entity_id': 'sensor.to_home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_no_trips_sensor[sensor.to_work-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.to_work', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:train', + 'original_name': 'To work', + 'platform': 'nederlandse_spoorwegen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01K721DZPMEN39R5DK0ATBMSY8-actual_departure', + 'unit_of_measurement': None, + }) +# --- +# name: test_no_trips_sensor[sensor.to_work-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'attribution': 'Data provided by NS', + 'device_class': 'timestamp', + 'friendly_name': 'To work', + 'icon': 'mdi:train', + }), + 'context': , + 'entity_id': 'sensor.to_work', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- # name: test_sensor[sensor.to_home-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -143,3 +245,147 @@ 'state': '2025-09-15T14:35:00+00:00', }) # --- +# name: test_single_trip_sensor[sensor.to_home-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.to_home', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:train', + 'original_name': 'To home', + 'platform': 'nederlandse_spoorwegen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01K721DZPMEN39R5DK0ATBMSY9-actual_departure', + 'unit_of_measurement': None, + }) +# --- +# name: test_single_trip_sensor[sensor.to_home-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'arrival_delay': False, + 'arrival_platform_actual': '13', + 'arrival_platform_planned': '13', + 'arrival_time_actual': '18:45', + 'arrival_time_planned': '18:45', + 'attribution': 'Data provided by NS', + 'departure_delay': True, + 'departure_platform_actual': '4', + 'departure_platform_planned': '4', + 'departure_time_actual': '16:35', + 'departure_time_planned': '16:34', + 'device_class': 'timestamp', + 'friendly_name': 'To home', + 'going': True, + 'icon': 'mdi:train', + 'next': None, + 'remarks': None, + 'route': list([ + 'Amsterdam Centraal', + "'s-Hertogenbosch", + 'Breda', + 'Rotterdam Centraal', + ]), + 'status': 'normal', + 'transfers': 2, + }), + 'context': , + 'entity_id': 'sensor.to_home', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-09-15T14:35:00+00:00', + }) +# --- +# name: test_single_trip_sensor[sensor.to_work-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.to_work', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': 'mdi:train', + 'original_name': 'To work', + 'platform': 'nederlandse_spoorwegen', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': '01K721DZPMEN39R5DK0ATBMSY8-actual_departure', + 'unit_of_measurement': None, + }) +# --- +# name: test_single_trip_sensor[sensor.to_work-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'arrival_delay': False, + 'arrival_platform_actual': '13', + 'arrival_platform_planned': '13', + 'arrival_time_actual': '18:45', + 'arrival_time_planned': '18:45', + 'attribution': 'Data provided by NS', + 'departure_delay': True, + 'departure_platform_actual': '4', + 'departure_platform_planned': '4', + 'departure_time_actual': '16:35', + 'departure_time_planned': '16:34', + 'device_class': 'timestamp', + 'friendly_name': 'To work', + 'going': True, + 'icon': 'mdi:train', + 'next': None, + 'remarks': None, + 'route': list([ + 'Amsterdam Centraal', + "'s-Hertogenbosch", + 'Breda', + 'Rotterdam Centraal', + ]), + 'status': 'normal', + 'transfers': 2, + }), + 'context': , + 'entity_id': 'sensor.to_work', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2025-09-15T14:35:00+00:00', + }) +# --- diff --git a/tests/components/nederlandse_spoorwegen/test_sensor.py b/tests/components/nederlandse_spoorwegen/test_sensor.py index 28839f633f1c..96110cbe0fbc 100644 --- a/tests/components/nederlandse_spoorwegen/test_sensor.py +++ b/tests/components/nederlandse_spoorwegen/test_sensor.py @@ -68,7 +68,35 @@ async def test_config_import( @pytest.mark.freeze_time("2025-09-15 14:30:00+00:00") async def test_sensor( hass: HomeAssistant, - mock_nsapi, + mock_nsapi: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test sensor initialization.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.freeze_time("2025-09-15 14:30:00+00:00") +async def test_single_trip_sensor( + hass: HomeAssistant, + mock_single_trip_nsapi: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, +) -> None: + """Test sensor initialization.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.freeze_time("2025-09-15 14:30:00+00:00") +async def test_no_trips_sensor( + hass: HomeAssistant, + mock_no_trips_nsapi: AsyncMock, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, entity_registry: er.EntityRegistry,