diff --git a/.claude/agents/quality-scale-rule-verifier.md b/.claude/agents/quality-scale-rule-verifier.md new file mode 100644 index 00000000000000..a86566dea684df --- /dev/null +++ b/.claude/agents/quality-scale-rule-verifier.md @@ -0,0 +1,77 @@ +--- +name: quality-scale-rule-verifier +description: | + Use this agent when you need to verify that a Home Assistant integration follows a specific quality scale rule. This includes checking if the integration implements required patterns, configurations, or code structures defined by the quality scale system. + + + Context: The user wants to verify if an integration follows a specific quality scale rule. + user: "Check if the peblar integration follows the config-flow rule" + assistant: "I'll use the quality scale rule verifier to check if the peblar integration properly implements the config-flow rule." + + Since the user is asking to verify a quality scale rule implementation, use the quality-scale-rule-verifier agent. + + + + + Context: The user is reviewing if an integration reaches a specific quality scale level. + user: "Verify that this integration reaches the bronze quality scale" + assistant: "Let me use the quality scale rule verifier to check the bronze quality scale implementation." + + The user wants to verify the integration has reached a certain quality level, so use multiple quality-scale-rule-verifier agents to verify each bronze rule. + + +model: inherit +color: yellow +tools: Read, Bash, Grep, Glob, WebFetch +--- + +You are an expert Home Assistant integration quality scale auditor specializing in verifying compliance with specific quality scale rules. You have deep knowledge of Home Assistant's architecture, best practices, and the quality scale system that ensures integration consistency and reliability. + +You will verify if an integration follows a specific quality scale rule by: + +1. **Fetching Rule Documentation**: Retrieve the official rule documentation from: + `https://raw.githubusercontent.com/home-assistant/developers.home-assistant/refs/heads/master/docs/core/integration-quality-scale/rules/{rule_name}.md` + where `{rule_name}` is the rule identifier (e.g., 'config-flow', 'entity-unique-id', 'parallel-updates') + +2. **Understanding Rule Requirements**: Parse the rule documentation to identify: + - Core requirements and mandatory implementations + - Specific code patterns or configurations required + - Common violations and anti-patterns + - Exemption criteria (when a rule might not apply) + - The quality tier this rule belongs to (Bronze, Silver, Gold, Platinum) + +3. **Analyzing Integration Code**: Examine the integration's codebase at `homeassistant/components/` focusing on: + - `manifest.json` for quality scale declaration and configuration + - `quality_scale.yaml` for rule status (done, todo, exempt) + - Relevant Python modules based on the rule requirements + - Configuration files and service definitions as needed + +4. **Verification Process**: + - Check if the rule is marked as 'done', 'todo', or 'exempt' in quality_scale.yaml + - If marked 'exempt', verify the exemption reason is valid + - If marked 'done', verify the actual implementation matches requirements + - Identify specific files and code sections that demonstrate compliance or violations + - Consider the integration's declared quality tier when applying rules + - To fetch the integration docs, use WebFetch to fetch from `https://raw.githubusercontent.com/home-assistant/home-assistant.io/refs/heads/current/source/_integrations/.markdown` + - To fetch information about a PyPI package, use the URL `https://pypi.org/pypi//json` + +5. **Reporting Findings**: Provide a comprehensive verification report that includes: + - **Rule Summary**: Brief description of what the rule requires + - **Compliance Status**: Clear pass/fail/exempt determination + - **Evidence**: Specific code examples showing compliance or violations + - **Issues Found**: Detailed list of any non-compliance issues with file locations + - **Recommendations**: Actionable steps to achieve compliance if needed + - **Exemption Analysis**: If applicable, whether the exemption is justified + +When examining code, you will: +- Look for exact implementation patterns specified in the rule +- Verify all required components are present and properly configured +- Check for common mistakes and anti-patterns +- Consider edge cases and error handling requirements +- Validate that implementations follow Home Assistant conventions + +You will be thorough but focused, examining only the aspects relevant to the specific rule being verified. You will provide clear, actionable feedback that helps developers understand both what needs to be fixed and why it matters for integration quality. + +If you cannot access the rule documentation or find the integration code, clearly state what information is missing and what you would need to complete the verification. + +Remember that quality scale rules are cumulative - Bronze rules apply to all integrations with a quality scale, Silver rules apply to Silver+ integrations, and so on. Always consider the integration's target quality level when determining which rules should be enforced. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 792dacd803249d..c93e07dfb4fb2c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -55,8 +55,12 @@ creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code. + + AI tools are welcome, but contributors are responsible for *fully* + understanding the code before submitting a PR. --> +- [ ] I understand the code I am submitting and can explain how it works. - [ ] The code change is tested and works locally. - [ ] Local tests pass. **Your PR cannot be merged unless tests pass** - [ ] There is no commented out code in this PR. @@ -64,6 +68,7 @@ - [ ] I have followed the [perfect PR recommendations][perfect-pr] - [ ] The code has been formatted using Ruff (`ruff format homeassistant tests`) - [ ] Tests have been added to verify that the new code works. +- [ ] Any generated code has been carefully reviewed for correctness and compliance with project standards. If user exposed functionality or configuration variables are added/changed: diff --git a/.gitignore b/.gitignore index 9bcf440a2f16d3..bcd3e3d95d08e6 100644 --- a/.gitignore +++ b/.gitignore @@ -140,5 +140,5 @@ tmp_cache pytest_buckets.txt # AI tooling -.claude +.claude/settings.local.json diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 0bbdfae50e4ea9..86cab723a07850 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==3.13.1"] + "requirements": ["bthome-ble==3.14.2"] } diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index dbabad960410ae..08d52efda0941a 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -25,6 +25,7 @@ DEGREE, LIGHT_LUX, PERCENTAGE, + REVOLUTIONS_PER_MINUTE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, EntityCategory, UnitOfConductivity, @@ -269,6 +270,15 @@ native_unit_of_measurement=DEGREE, state_class=SensorStateClass.MEASUREMENT, ), + # Rotational speed (rpm) + ( + BTHomeExtendedSensorDeviceClass.ROTATIONAL_SPEED, + Units.REVOLUTIONS_PER_MINUTE, + ): SensorEntityDescription( + key=f"{BTHomeExtendedSensorDeviceClass.ROTATIONAL_SPEED}_{Units.REVOLUTIONS_PER_MINUTE}", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + ), # Signal Strength (RSSI) (dB) ( BTHomeSensorDeviceClass.SIGNAL_STRENGTH, diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index c486a385721c44..8eeff19ce4f578 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -120,6 +120,14 @@ def _make_url_from_data(data: dict[str, str]) -> str: return f"{protocol}{address}" +def _get_protocol_from_url(url: str) -> str: + """Get protocol from URL. Returns the configured protocol from URL or the default secure protocol.""" + return next( + (k for k, v in PROTOCOL_MAP.items() if url.startswith(v)), + DEFAULT_SECURE_PROTOCOL, + ) + + def _placeholders_from_device(device: ElkSystem) -> dict[str, str]: return { "mac_address": _short_mac(device.mac_address), @@ -205,6 +213,78 @@ async def async_step_discovery_confirm( ) return await self.async_step_discovered_connection() + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reconfiguration of the integration.""" + errors: dict[str, str] = {} + reconfigure_entry = self._get_reconfigure_entry() + existing_data = reconfigure_entry.data + + if user_input is not None: + validate_input_data = dict(user_input) + validate_input_data[CONF_PREFIX] = existing_data.get(CONF_PREFIX, "") + + try: + info = await validate_input( + validate_input_data, reconfigure_entry.unique_id + ) + except TimeoutError: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors[CONF_PASSWORD] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception during reconfiguration") + errors["base"] = "unknown" + else: + # Discover the device at the provided address to obtain its MAC (unique_id) + device = await async_discover_device( + self.hass, validate_input_data[CONF_ADDRESS] + ) + if device is not None and device.mac_address: + await self.async_set_unique_id(dr.format_mac(device.mac_address)) + self._abort_if_unique_id_mismatch() # aborts if user tried to switch devices + else: + # If we cannot confirm identity, keep existing behavior (don't block reconfigure) + await self.async_set_unique_id(reconfigure_entry.unique_id) + + return self.async_update_reload_and_abort( + reconfigure_entry, + data_updates={ + **reconfigure_entry.data, + CONF_HOST: info[CONF_HOST], + CONF_USERNAME: validate_input_data[CONF_USERNAME], + CONF_PASSWORD: validate_input_data[CONF_PASSWORD], + CONF_PREFIX: info[CONF_PREFIX], + }, + reason="reconfigure_successful", + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=vol.Schema( + { + vol.Optional( + CONF_USERNAME, + default=existing_data.get(CONF_USERNAME, ""), + ): str, + vol.Optional( + CONF_PASSWORD, + default="", + ): str, + vol.Required( + CONF_ADDRESS, + default=hostname_from_url(existing_data[CONF_HOST]), + ): str, + vol.Required( + CONF_PROTOCOL, + default=_get_protocol_from_url(existing_data[CONF_HOST]), + ): vol.In(ALL_PROTOCOLS), + } + ), + errors=errors, + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -249,12 +329,14 @@ async def _async_create_or_error( try: info = await validate_input(user_input, self.unique_id) - except TimeoutError: + except TimeoutError as ex: + _LOGGER.debug("Connection timed out: %s", ex) return {"base": "cannot_connect"}, None - except InvalidAuth: + except InvalidAuth as ex: + _LOGGER.debug("Invalid auth for %s: %s", user_input.get(CONF_HOST), ex) return {CONF_PASSWORD: "invalid_auth"}, None except Exception: - _LOGGER.exception("Unexpected exception") + _LOGGER.exception("Unexpected error validating input") return {"base": "unknown"}, None if importing: diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json index 19967612b0f207..400a7197f41125 100644 --- a/homeassistant/components/elkm1/strings.json +++ b/homeassistant/components/elkm1/strings.json @@ -17,8 +17,8 @@ "address": "The IP address or domain or serial port if connecting via serial.", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "prefix": "A unique prefix (leave blank if you only have one ElkM1).", - "temperature_unit": "The temperature unit ElkM1 uses." + "prefix": "A unique prefix (leave blank if you only have one Elk-M1).", + "temperature_unit": "The temperature unit Elk-M1 uses." } }, "discovered_connection": { @@ -30,6 +30,16 @@ "password": "[%key:common::config_flow::data::password%]", "temperature_unit": "[%key:component::elkm1::config::step::manual_connection::data::temperature_unit%]" } + }, + "reconfigure": { + "title": "Reconfigure Elk-M1 Control", + "description": "[%key:component::elkm1::config::step::manual_connection::description%]", + "data": { + "protocol": "[%key:component::elkm1::config::step::manual_connection::data::protocol%]", + "address": "[%key:component::elkm1::config::step::manual_connection::data::address%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -42,8 +52,10 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "already_configured": "An ElkM1 with this prefix is already configured", - "address_already_configured": "An ElkM1 with this address is already configured" + "already_configured": "An Elk-M1 with this prefix is already configured", + "address_already_configured": "An Elk-M1 with this address is already configured", + "reconfigure_successful": "Successfully reconfigured Elk-M1 integration", + "unique_id_mismatch": "Reconfigure should be used for the same device not a new one" } }, "services": { @@ -69,7 +81,7 @@ }, "alarm_arm_home_instant": { "name": "Alarm arm home instant", - "description": "Arms the ElkM1 in home instant mode.", + "description": "Arms the Elk-M1 in home instant mode.", "fields": { "code": { "name": "Code", @@ -79,7 +91,7 @@ }, "alarm_arm_night_instant": { "name": "Alarm arm night instant", - "description": "Arms the ElkM1 in night instant mode.", + "description": "Arms the Elk-M1 in night instant mode.", "fields": { "code": { "name": "Code", @@ -89,7 +101,7 @@ }, "alarm_arm_vacation": { "name": "Alarm arm vacation", - "description": "Arms the ElkM1 in vacation mode.", + "description": "Arms the Elk-M1 in vacation mode.", "fields": { "code": { "name": "Code", @@ -99,7 +111,7 @@ }, "alarm_display_message": { "name": "Alarm display message", - "description": "Displays a message on all of the ElkM1 keypads for an area.", + "description": "Displays a message on all of the Elk-M1 keypads for an area.", "fields": { "clear": { "name": "Clear", @@ -135,7 +147,7 @@ }, "speak_phrase": { "name": "Speak phrase", - "description": "Speaks a phrase. See list of phrases in ElkM1 ASCII Protocol documentation.", + "description": "Speaks a phrase. See list of phrases in Elk-M1 ASCII Protocol documentation.", "fields": { "number": { "name": "Phrase number", @@ -149,7 +161,7 @@ }, "speak_word": { "name": "Speak word", - "description": "Speaks a word. See list of words in ElkM1 ASCII Protocol documentation.", + "description": "Speaks a word. See list of words in Elk-M1 ASCII Protocol documentation.", "fields": { "number": { "name": "Word number", diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 1f7d37dd8a614f..c95ef6b1c63541 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -22,5 +22,5 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["eq3btsmart"], - "requirements": ["eq3btsmart==2.2.0"] + "requirements": ["eq3btsmart==2.3.0"] } diff --git a/homeassistant/components/homeassistant_hardware/manifest.json b/homeassistant/components/homeassistant_hardware/manifest.json index cf9acf14a5da9a..26d227ae922d86 100644 --- a/homeassistant/components/homeassistant_hardware/manifest.json +++ b/homeassistant/components/homeassistant_hardware/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware", "integration_type": "system", "requirements": [ - "universal-silabs-flasher==0.0.31", + "universal-silabs-flasher==0.0.32", "ha-silabs-firmware-client==0.2.0" ] } diff --git a/homeassistant/components/improv_ble/strings.json b/homeassistant/components/improv_ble/strings.json index be157b8070deeb..08f25fb5947d10 100644 --- a/homeassistant/components/improv_ble/strings.json +++ b/homeassistant/components/improv_ble/strings.json @@ -42,7 +42,7 @@ "characteristic_missing": "The device is either already connected to Wi-Fi, or no longer able to connect to Wi-Fi. If you want to connect it to another network, try factory resetting it first.", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "provision_successful": "The device has successfully connected to the Wi-Fi network.", - "provision_successful_url": "The device has successfully connected to the Wi-Fi network.\n\nPlease visit {url} to finish setup.", + "provision_successful_url": "The device has successfully connected to the Wi-Fi network.\n\nPlease [click here]({url}) to finish setup.", "unknown": "[%key:common::config_flow::error::unknown%]" } } diff --git a/homeassistant/components/matter/const.py b/homeassistant/components/matter/const.py index 4c4679b0042ed0..8018d5e09edf7e 100644 --- a/homeassistant/components/matter/const.py +++ b/homeassistant/components/matter/const.py @@ -15,8 +15,3 @@ ID_TYPE_SERIAL = "serial" FEATUREMAP_ATTRIBUTE_ID = 65532 - -# vacuum entity service actions -SERVICE_GET_AREAS = "get_areas" # get SupportedAreas and SupportedMaps -SERVICE_SELECT_AREAS = "select_areas" # call SelectAreas Matter command -SERVICE_CLEAN_AREAS = "clean_areas" # call SelectAreas Matter command and start RVC diff --git a/homeassistant/components/matter/icons.json b/homeassistant/components/matter/icons.json index a19b476914d869..dc1fbc25181f3c 100644 --- a/homeassistant/components/matter/icons.json +++ b/homeassistant/components/matter/icons.json @@ -150,16 +150,5 @@ "default": "mdi:ev-station" } } - }, - "services": { - "clean_areas": { - "service": "mdi:robot-vacuum" - }, - "get_areas": { - "service": "mdi:map" - }, - "select_areas": { - "service": "mdi:map" - } } } diff --git a/homeassistant/components/matter/services.yaml b/homeassistant/components/matter/services.yaml deleted file mode 100644 index d0e5767315916f..00000000000000 --- a/homeassistant/components/matter/services.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Service descriptions for Matter integration - -get_areas: - target: - entity: - domain: vacuum - -select_areas: - target: - entity: - domain: vacuum - fields: - areas: - required: true - example: [1, 3] - -clean_areas: - target: - entity: - domain: vacuum - fields: - areas: - required: true - example: [1, 3] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index ff3b52bf473c60..7dae7638d8d623 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -548,30 +548,6 @@ "description": "The Matter device to add to the other Matter network." } } - }, - "get_areas": { - "name": "Get areas", - "description": "Returns a list of available areas and maps for robot vacuum cleaners." - }, - "select_areas": { - "name": "Select areas", - "description": "Selects the specified areas for cleaning. The areas must be specified as a list of area IDs.", - "fields": { - "areas": { - "name": "Areas", - "description": "A list of area IDs to select." - } - } - }, - "clean_areas": { - "name": "Clean areas", - "description": "Instructs the Matter vacuum cleaner to clean the specified areas.", - "fields": { - "areas": { - "name": "Areas", - "description": "A list of area IDs to clean." - } - } } } } diff --git a/homeassistant/components/matter/vacuum.py b/homeassistant/components/matter/vacuum.py index c9c56df9894c9e..cf9f26adecb4d7 100644 --- a/homeassistant/components/matter/vacuum.py +++ b/homeassistant/components/matter/vacuum.py @@ -3,12 +3,10 @@ from __future__ import annotations from enum import IntEnum -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any from chip.clusters import Objects as clusters -from chip.clusters.Objects import NullValue from matter_server.client.models import device_types -import voluptuous as vol from homeassistant.components.vacuum import ( StateVacuumEntity, @@ -18,25 +16,14 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import ( - HomeAssistant, - ServiceResponse, - SupportsResponse, - callback, -) +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .const import SERVICE_CLEAN_AREAS, SERVICE_GET_AREAS, SERVICE_SELECT_AREAS from .entity import MatterEntity from .helpers import get_matter from .models import MatterDiscoverySchema -ATTR_CURRENT_AREA = "current_area" -ATTR_CURRENT_AREA_NAME = "current_area_name" -ATTR_SELECTED_AREAS = "selected_areas" - class OperationalState(IntEnum): """Operational State of the vacuum cleaner. @@ -69,33 +56,6 @@ async def async_setup_entry( """Set up Matter vacuum platform from Config Entry.""" matter = get_matter(hass) matter.register_platform_handler(Platform.VACUUM, async_add_entities) - platform = entity_platform.async_get_current_platform() - - # This will call Entity.async_handle_get_areas - platform.async_register_entity_service( - SERVICE_GET_AREAS, - schema=None, - func="async_handle_get_areas", - supports_response=SupportsResponse.ONLY, - ) - # This will call Entity.async_handle_clean_areas - platform.async_register_entity_service( - SERVICE_CLEAN_AREAS, - schema={ - vol.Required("areas"): vol.All(cv.ensure_list, [cv.positive_int]), - }, - func="async_handle_clean_areas", - supports_response=SupportsResponse.ONLY, - ) - # This will call Entity.async_handle_select_areas - platform.async_register_entity_service( - SERVICE_SELECT_AREAS, - schema={ - vol.Required("areas"): vol.All(cv.ensure_list, [cv.positive_int]), - }, - func="async_handle_select_areas", - supports_response=SupportsResponse.ONLY, - ) class MatterVacuum(MatterEntity, StateVacuumEntity): @@ -105,23 +65,9 @@ class MatterVacuum(MatterEntity, StateVacuumEntity): _supported_run_modes: ( dict[int, clusters.RvcRunMode.Structs.ModeOptionStruct] | None ) = None - _attr_matter_areas: dict[str, Any] | None = None - _attr_current_area: int | None = None - _attr_current_area_name: str | None = None - _attr_selected_areas: list[int] | None = None - _attr_supported_maps: list[dict[str, Any]] | None = None entity_description: StateVacuumEntityDescription _platform_translation_key = "vacuum" - @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return the state attributes of the entity.""" - return { - ATTR_CURRENT_AREA: self._attr_current_area, - ATTR_CURRENT_AREA_NAME: self._attr_current_area_name, - ATTR_SELECTED_AREAS: self._attr_selected_areas, - } - def _get_run_mode_by_tag( self, tag: ModeTag ) -> clusters.RvcRunMode.Structs.ModeOptionStruct | None: @@ -190,160 +136,10 @@ async def async_pause(self) -> None: """Pause the cleaning task.""" await self.send_device_command(clusters.RvcOperationalState.Commands.Pause()) - def async_get_areas(self, **kwargs: Any) -> dict[str, Any]: - """Get available area and map IDs from vacuum appliance.""" - - supported_areas = self.get_matter_attribute_value( - clusters.ServiceArea.Attributes.SupportedAreas - ) - if not supported_areas: - raise HomeAssistantError("Can't get areas from the device.") - - # Group by area_id: {area_id: {"map_id": ..., "name": ...}} - areas = {} - for area in supported_areas: - area_id = getattr(area, "areaID", None) - map_id = getattr(area, "mapID", None) - location_name = None - area_info = getattr(area, "areaInfo", None) - if area_info is not None: - location_info = getattr(area_info, "locationInfo", None) - if location_info is not None: - location_name = getattr(location_info, "locationName", None) - if area_id is not None: - areas[area_id] = {"map_id": map_id, "name": location_name} - - # Optionally, also extract supported maps if available - supported_maps = self.get_matter_attribute_value( - clusters.ServiceArea.Attributes.SupportedMaps - ) - maps = [] - if supported_maps: - maps = [ - { - "map_id": getattr(m, "mapID", None), - "name": getattr(m, "name", None), - } - for m in supported_maps - ] - - return { - "areas": areas, - "maps": maps, - } - - async def async_handle_get_areas(self, **kwargs: Any) -> ServiceResponse: - """Get available area and map IDs from vacuum appliance.""" - # Group by area_id: {area_id: {"map_id": ..., "name": ...}} - areas = {} - if self._attr_matter_areas is not None: - for area in self._attr_matter_areas: - area_id = getattr(area, "areaID", None) - map_id = getattr(area, "mapID", None) - location_name = None - area_info = getattr(area, "areaInfo", None) - if area_info is not None: - location_info = getattr(area_info, "locationInfo", None) - if location_info is not None: - location_name = getattr(location_info, "locationName", None) - if area_id is not None: - if map_id is NullValue: - areas[area_id] = {"name": location_name} - else: - areas[area_id] = {"map_id": map_id, "name": location_name} - - # Optionally, also extract supported maps if available - supported_maps = self.get_matter_attribute_value( - clusters.ServiceArea.Attributes.SupportedMaps - ) - maps = [] - if supported_maps != NullValue: # chip.clusters.Types.Nullable - maps = [ - { - "map_id": getattr(m, "mapID", None) - if getattr(m, "mapID", None) != NullValue - else None, - "name": getattr(m, "name", None), - } - for m in supported_maps - ] - - return cast( - ServiceResponse, - { - "areas": areas, - "maps": maps, - }, - ) - return None - - async def async_handle_select_areas( - self, areas: list[int], **kwargs: Any - ) -> ServiceResponse: - """Select areas to clean.""" - selected_areas = areas - # Matter command to the vacuum cleaner to select the areas. - await self.send_device_command( - clusters.ServiceArea.Commands.SelectAreas(newAreas=selected_areas) - ) - # Return response indicating selected areas. - return cast( - ServiceResponse, {"status": "areas selected", "areas": selected_areas} - ) - - async def async_handle_clean_areas( - self, areas: list[int], **kwargs: Any - ) -> ServiceResponse: - """Start cleaning the specified areas.""" - # Matter command to the vacuum cleaner to select the areas. - await self.send_device_command( - clusters.ServiceArea.Commands.SelectAreas(newAreas=areas) - ) - # Start the vacuum cleaner after selecting areas. - await self.async_start() - # Return response indicating selected areas. - return cast( - ServiceResponse, {"status": "cleaning areas selected", "areas": areas} - ) - @callback def _update_from_device(self) -> None: """Update from device.""" self._calculate_features() - # ServiceArea: get areas from the device - self._attr_matter_areas = self.get_matter_attribute_value( - clusters.ServiceArea.Attributes.SupportedAreas - ) - # optional CurrentArea attribute - # pylint: disable=too-many-nested-blocks - if self.get_matter_attribute_value(clusters.ServiceArea.Attributes.CurrentArea): - current_area = self.get_matter_attribute_value( - clusters.ServiceArea.Attributes.CurrentArea - ) - # get areaInfo.locationInfo.locationName for current_area in SupportedAreas list - area_name = None - if self._attr_matter_areas: - for area in self._attr_matter_areas: - if getattr(area, "areaID", None) == current_area: - area_info = getattr(area, "areaInfo", None) - if area_info is not None: - location_info = getattr(area_info, "locationInfo", None) - if location_info is not None: - area_name = getattr(location_info, "locationName", None) - break - self._attr_current_area = current_area - self._attr_current_area_name = area_name - else: - self._attr_current_area = None - self._attr_current_area_name = None - - # optional SelectedAreas attribute - if self.get_matter_attribute_value( - clusters.ServiceArea.Attributes.SelectedAreas - ): - self._attr_selected_areas = self.get_matter_attribute_value( - clusters.ServiceArea.Attributes.SelectedAreas - ) # derive state from the run mode + operational state run_mode_raw: int = self.get_matter_attribute_value( clusters.RvcRunMode.Attributes.CurrentMode @@ -424,10 +220,6 @@ def _calculate_features(self) -> None: clusters.RvcRunMode.Attributes.CurrentMode, clusters.RvcOperationalState.Attributes.OperationalState, ), - optional_attributes=( - clusters.ServiceArea.Attributes.SelectedAreas, - clusters.ServiceArea.Attributes.CurrentArea, - ), device_type=(device_types.RoboticVacuumCleaner,), allow_none_value=True, ), diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index f71a333dbe1085..debc558dec59b7 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -91,8 +91,6 @@ _LOGGER = logging.getLogger(__name__) -DOMAIN = "mqtt_json" - DEFAULT_NAME = "MQTT JSON Light" DEFAULT_FLASH = True diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 28016242a6a30b..686864606a92b8 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/p1_monitor", "iot_class": "local_polling", "loggers": ["p1monitor"], - "requirements": ["p1monitor==3.1.0"] + "requirements": ["p1monitor==3.2.0"] } diff --git a/homeassistant/components/playstation_network/manifest.json b/homeassistant/components/playstation_network/manifest.json index 590bd73fbf7f67..559d91b82fb8b6 100644 --- a/homeassistant/components/playstation_network/manifest.json +++ b/homeassistant/components/playstation_network/manifest.json @@ -81,5 +81,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["PSNAWP==3.0.0", "pyrate-limiter==3.7.0"] + "requirements": ["PSNAWP==3.0.0", "pyrate-limiter==3.9.0"] } diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index 2cc243323a1043..e62fe0325ccb06 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -16,6 +16,7 @@ Unresolvable, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.util.location import vincenty from . import RadioBrowserConfigEntry from .const import DOMAIN @@ -88,6 +89,7 @@ async def async_browse_media( *await self._async_build_popular(radios, item), *await self._async_build_by_tag(radios, item), *await self._async_build_by_language(radios, item), + *await self._async_build_local(radios, item), *await self._async_build_by_country(radios, item), ], ) @@ -292,3 +294,63 @@ async def _async_build_by_tag( ] return [] + + def _filter_local_stations( + self, stations: list[Station], latitude: float, longitude: float + ) -> list[Station]: + return [ + station + for station in stations + if station.latitude is not None + and station.longitude is not None + and ( + ( + dist := vincenty( + (latitude, longitude), + (station.latitude, station.longitude), + False, + ) + ) + is not None + ) + and dist < 100 + ] + + async def _async_build_local( + self, radios: RadioBrowser, item: MediaSourceItem + ) -> list[BrowseMediaSource]: + """Handle browsing local radio stations.""" + + if item.identifier == "local": + country = self.hass.config.country + stations = await radios.stations( + filter_by=FilterBy.COUNTRY_CODE_EXACT, + filter_term=country, + hide_broken=True, + order=Order.NAME, + reverse=False, + ) + + local_stations = await self.hass.async_add_executor_job( + self._filter_local_stations, + stations, + self.hass.config.latitude, + self.hass.config.longitude, + ) + + return self._async_build_stations(radios, local_stations) + + if not item.identifier: + return [ + BrowseMediaSource( + domain=DOMAIN, + identifier="local", + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, + title="Local stations", + can_play=False, + can_expand=True, + ) + ] + + return [] diff --git a/requirements_all.txt b/requirements_all.txt index 83d8ef5e5bc811..6182cc9be6e130 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -700,7 +700,7 @@ brunt==1.2.0 bt-proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==3.13.1 +bthome-ble==3.14.2 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 @@ -904,7 +904,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==2.2.0 +eq3btsmart==2.3.0 # homeassistant.components.esphome esphome-dashboard-api==1.3.0 @@ -1652,7 +1652,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.1 # homeassistant.components.p1_monitor -p1monitor==3.1.0 +p1monitor==3.2.0 # homeassistant.components.mqtt paho-mqtt==2.1.0 @@ -2296,7 +2296,7 @@ pyrail==0.4.1 pyrainbird==6.0.1 # homeassistant.components.playstation_network -pyrate-limiter==3.7.0 +pyrate-limiter==3.9.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 @@ -3032,7 +3032,7 @@ unifi_ap==0.0.2 unifiled==0.11 # homeassistant.components.homeassistant_hardware -universal-silabs-flasher==0.0.31 +universal-silabs-flasher==0.0.32 # homeassistant.components.upb upb-lib==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0f5728f11d59f..eb303cdcf87cb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -624,7 +624,7 @@ brottsplatskartan==1.0.5 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==3.13.1 +bthome-ble==3.14.2 # homeassistant.components.buienradar buienradar==1.0.6 @@ -786,7 +786,7 @@ epion==0.0.3 epson-projector==0.5.1 # homeassistant.components.eq3btsmart -eq3btsmart==2.2.0 +eq3btsmart==2.3.0 # homeassistant.components.esphome esphome-dashboard-api==1.3.0 @@ -1405,7 +1405,7 @@ ourgroceries==1.5.4 ovoenergy==2.0.1 # homeassistant.components.p1_monitor -p1monitor==3.1.0 +p1monitor==3.2.0 # homeassistant.components.mqtt paho-mqtt==2.1.0 @@ -1920,7 +1920,7 @@ pyrail==0.4.1 pyrainbird==6.0.1 # homeassistant.components.playstation_network -pyrate-limiter==3.7.0 +pyrate-limiter==3.9.0 # homeassistant.components.risco pyrisco==0.6.7 @@ -2503,7 +2503,7 @@ ultraheat-api==0.5.7 unifi-discovery==1.2.0 # homeassistant.components.homeassistant_hardware -universal-silabs-flasher==0.0.31 +universal-silabs-flasher==0.0.32 # homeassistant.components.upb upb-lib==0.6.1 diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 548f374010ea3c..fab0cdf10c9dbe 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Elk-M1 Control config flow.""" +from __future__ import annotations + from dataclasses import asdict from unittest.mock import patch @@ -7,8 +9,15 @@ import pytest from homeassistant import config_entries -from homeassistant.components.elkm1.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.components.elkm1.const import CONF_AUTO_CONFIGURE, DOMAIN +from homeassistant.const import ( + CONF_ADDRESS, + CONF_HOST, + CONF_PASSWORD, + CONF_PREFIX, + CONF_PROTOCOL, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr @@ -36,6 +45,21 @@ MODULE = "homeassistant.components.elkm1" +@pytest.fixture +def mock_config_entry(): + """Create a mock config entry for testing.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: f"elks://{MOCK_IP_ADDRESS}", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + }, + unique_id=MOCK_MAC, + ) + + async def test_discovery_ignored_entry(hass: HomeAssistant) -> None: """Test we abort on ignored entry.""" config_entry = MockConfigEntry( @@ -86,11 +110,11 @@ async def test_form_user_with_secure_elk_no_discovery(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "secure", - "address": "1.2.3.4", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) await hass.async_block_till_done() @@ -98,11 +122,11 @@ async def test_form_user_with_secure_elk_no_discovery(hass: HomeAssistant) -> No assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { - "auto_configure": True, - "host": "elks://1.2.3.4", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://1.2.3.4", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -143,11 +167,11 @@ async def test_form_user_with_insecure_elk_skip_discovery(hass: HomeAssistant) - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "non-secure", - "address": "1.2.3.4", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "non-secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) await hass.async_block_till_done() @@ -155,11 +179,11 @@ async def test_form_user_with_insecure_elk_skip_discovery(hass: HomeAssistant) - assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { - "auto_configure": True, - "host": "elk://1.2.3.4", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://1.2.3.4", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -200,11 +224,11 @@ async def test_form_user_with_insecure_elk_no_discovery(hass: HomeAssistant) -> result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "non-secure", - "address": "1.2.3.4", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "non-secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) await hass.async_block_till_done() @@ -212,11 +236,11 @@ async def test_form_user_with_insecure_elk_no_discovery(hass: HomeAssistant) -> assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { - "auto_configure": True, - "host": "elk://1.2.3.4", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://1.2.3.4", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -255,11 +279,11 @@ async def test_form_user_with_insecure_elk_times_out(hass: HomeAssistant) -> Non result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "non-secure", - "address": "1.2.3.4", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "non-secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) await hass.async_block_till_done() @@ -267,6 +291,44 @@ async def test_form_user_with_insecure_elk_times_out(hass: HomeAssistant) -> Non assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} + # Retry with successful connection + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_discovery(), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PROTOCOL: "non-secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "ElkM1" + assert result3["data"] == { + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://1.2.3.4", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + async def test_form_user_with_secure_elk_no_discovery_ip_already_configured( hass: HomeAssistant, @@ -295,11 +357,11 @@ async def test_form_user_with_secure_elk_no_discovery_ip_already_configured( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "secure", - "address": "127.0.0.1", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "127.0.0.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) await hass.async_block_till_done() @@ -344,8 +406,8 @@ async def test_form_user_with_secure_elk_with_discovery(hass: HomeAssistant) -> result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) await hass.async_block_till_done() @@ -353,11 +415,11 @@ async def test_form_user_with_secure_elk_with_discovery(hass: HomeAssistant) -> assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { - "auto_configure": True, - "host": "elks://127.0.0.1", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://127.0.0.1", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert result3["result"].unique_id == "aa:bb:cc:dd:ee:ff" assert len(mock_setup.mock_calls) == 1 @@ -402,11 +464,11 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual( result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - "protocol": "secure", - "address": "1.2.3.4", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) await hass.async_block_till_done() @@ -414,11 +476,11 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual( assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1" assert result3["data"] == { - "auto_configure": True, - "host": "elks://1.2.3.4", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://1.2.3.4", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert result3["result"].unique_id is None assert len(mock_setup.mock_calls) == 1 @@ -463,11 +525,11 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - "protocol": "secure", - "address": "127.0.0.1", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "127.0.0.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) await hass.async_block_till_done() @@ -475,11 +537,11 @@ async def test_form_user_with_secure_elk_with_discovery_pick_manual_direct_disco assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { - "auto_configure": True, - "host": "elks://127.0.0.1", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://127.0.0.1", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert result3["result"].unique_id == MOCK_MAC assert len(mock_setup.mock_calls) == 1 @@ -515,11 +577,11 @@ async def test_form_user_with_tls_elk_no_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "TLS 1.2", - "address": "1.2.3.4", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "TLS 1.2", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) await hass.async_block_till_done() @@ -527,11 +589,11 @@ async def test_form_user_with_tls_elk_no_discovery(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { - "auto_configure": True, - "host": "elksv1_2://1.2.3.4", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elksv1_2://1.2.3.4", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -566,9 +628,9 @@ async def test_form_user_with_non_secure_elk_no_discovery(hass: HomeAssistant) - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "non-secure", - "address": "1.2.3.4", - "prefix": "guest_house", + CONF_PROTOCOL: "non-secure", + CONF_ADDRESS: "1.2.3.4", + CONF_PREFIX: "guest_house", }, ) await hass.async_block_till_done() @@ -576,11 +638,11 @@ async def test_form_user_with_non_secure_elk_no_discovery(hass: HomeAssistant) - assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "guest_house" assert result2["data"] == { - "auto_configure": True, - "host": "elk://1.2.3.4", - "prefix": "guest_house", - "username": "", - "password": "", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://1.2.3.4", + CONF_PREFIX: "guest_house", + CONF_USERNAME: "", + CONF_PASSWORD: "", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -615,9 +677,9 @@ async def test_form_user_with_serial_elk_no_discovery(hass: HomeAssistant) -> No result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "serial", - "address": "/dev/ttyS0:115200", - "prefix": "", + CONF_PROTOCOL: "serial", + CONF_ADDRESS: "/dev/ttyS0:115200", + CONF_PREFIX: "", }, ) await hass.async_block_till_done() @@ -625,11 +687,11 @@ async def test_form_user_with_serial_elk_no_discovery(hass: HomeAssistant) -> No assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1" assert result2["data"] == { - "auto_configure": True, - "host": "serial:///dev/ttyS0:115200", - "prefix": "", - "username": "", - "password": "", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "serial:///dev/ttyS0:115200", + CONF_PREFIX: "", + CONF_USERNAME: "", + CONF_PASSWORD: "", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -659,17 +721,55 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "secure", - "address": "1.2.3.4", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} + # Retry with successful connection + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "ElkM1" + assert result3["data"] == { + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://1.2.3.4", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + async def test_unknown_exception(hass: HomeAssistant) -> None: """Test we handle an unknown exception during connecting.""" @@ -695,17 +795,56 @@ async def test_unknown_exception(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "secure", - "address": "1.2.3.4", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) assert result2["type"] is FlowResultType.FORM + # Simulate an unexpected exception (ValueError) and verify the flow returns an "unknown" error assert result2["errors"] == {"base": "unknown"} + # Retry with successful connection + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "ElkM1" + assert result3["data"] == { + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://1.2.3.4", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + async def test_form_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth error.""" @@ -722,17 +861,55 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "secure", - "address": "1.2.3.4", - "username": "test-username", - "password": "test-password", - "prefix": "", + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", }, ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} + # Retry with valid auth + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "correct-password", + CONF_PREFIX: "", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "ElkM1" + assert result3["data"] == { + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://1.2.3.4", + CONF_PASSWORD: "correct-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + async def test_form_invalid_auth_no_password(hass: HomeAssistant) -> None: """Test we handle invalid auth error when no password is provided.""" @@ -749,17 +926,55 @@ async def test_form_invalid_auth_no_password(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "secure", - "address": "1.2.3.4", - "username": "test-username", - "password": "", - "prefix": "", + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "", + CONF_PREFIX: "", }, ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} + # Retry with valid password + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "correct-password", + CONF_PREFIX: "", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.CREATE_ENTRY + assert result3["title"] == "ElkM1" + assert result3["data"] == { + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://1.2.3.4", + CONF_PASSWORD: "correct-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + async def test_form_import(hass: HomeAssistant) -> None: """Test we get the form with import source.""" @@ -780,11 +995,11 @@ async def test_form_import(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={ - "host": "elks://1.2.3.4", - "username": "friend", - "password": "love", + CONF_HOST: "elks://1.2.3.4", + CONF_USERNAME: "friend", + CONF_PASSWORD: "love", "temperature_unit": "C", - "auto_configure": False, + CONF_AUTO_CONFIGURE: False, "keypad": { "enabled": True, "exclude": [], @@ -793,7 +1008,7 @@ async def test_form_import(hass: HomeAssistant) -> None: "output": {"enabled": False, "exclude": [], "include": []}, "counter": {"enabled": False, "exclude": [], "include": []}, "plc": {"enabled": False, "exclude": [], "include": []}, - "prefix": "ohana", + CONF_PREFIX: "ohana", "setting": {"enabled": False, "exclude": [], "include": []}, "area": {"enabled": False, "exclude": [], "include": []}, "task": {"enabled": False, "exclude": [], "include": []}, @@ -811,20 +1026,20 @@ async def test_form_import(hass: HomeAssistant) -> None: assert result["title"] == "ohana" assert result["data"] == { - "auto_configure": False, - "host": "elks://1.2.3.4", + CONF_AUTO_CONFIGURE: False, + CONF_HOST: "elks://1.2.3.4", "keypad": {"enabled": True, "exclude": [], "include": [[1, 1], [2, 2], [3, 3]]}, "output": {"enabled": False, "exclude": [], "include": []}, - "password": "love", + CONF_PASSWORD: "love", "plc": {"enabled": False, "exclude": [], "include": []}, - "prefix": "ohana", + CONF_PREFIX: "ohana", "setting": {"enabled": False, "exclude": [], "include": []}, "area": {"enabled": False, "exclude": [], "include": []}, "counter": {"enabled": False, "exclude": [], "include": []}, "task": {"enabled": False, "exclude": [], "include": []}, "temperature_unit": "C", "thermostat": {"enabled": False, "exclude": [], "include": []}, - "username": "friend", + CONF_USERNAME: "friend", "zone": {"enabled": True, "exclude": [[15, 15], [28, 208]], "include": []}, } assert len(mock_setup.mock_calls) == 1 @@ -850,11 +1065,11 @@ async def test_form_import_device_discovered(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={ - "host": "elks://127.0.0.1", - "username": "friend", - "password": "love", + CONF_HOST: "elks://127.0.0.1", + CONF_USERNAME: "friend", + CONF_PASSWORD: "love", "temperature_unit": "C", - "auto_configure": False, + CONF_AUTO_CONFIGURE: False, "keypad": { "enabled": True, "exclude": [], @@ -863,7 +1078,7 @@ async def test_form_import_device_discovered(hass: HomeAssistant) -> None: "output": {"enabled": False, "exclude": [], "include": []}, "counter": {"enabled": False, "exclude": [], "include": []}, "plc": {"enabled": False, "exclude": [], "include": []}, - "prefix": "ohana", + CONF_PREFIX: "ohana", "setting": {"enabled": False, "exclude": [], "include": []}, "area": {"enabled": False, "exclude": [], "include": []}, "task": {"enabled": False, "exclude": [], "include": []}, @@ -881,20 +1096,20 @@ async def test_form_import_device_discovered(hass: HomeAssistant) -> None: assert result["title"] == "ohana" assert result["result"].unique_id == MOCK_MAC assert result["data"] == { - "auto_configure": False, - "host": "elks://127.0.0.1", + CONF_AUTO_CONFIGURE: False, + CONF_HOST: "elks://127.0.0.1", "keypad": {"enabled": True, "exclude": [], "include": [[1, 1], [2, 2], [3, 3]]}, "output": {"enabled": False, "exclude": [], "include": []}, - "password": "love", + CONF_PASSWORD: "love", "plc": {"enabled": False, "exclude": [], "include": []}, - "prefix": "ohana", + CONF_PREFIX: "ohana", "setting": {"enabled": False, "exclude": [], "include": []}, "area": {"enabled": False, "exclude": [], "include": []}, "counter": {"enabled": False, "exclude": [], "include": []}, "task": {"enabled": False, "exclude": [], "include": []}, "temperature_unit": "C", "thermostat": {"enabled": False, "exclude": [], "include": []}, - "username": "friend", + CONF_USERNAME: "friend", "zone": {"enabled": True, "exclude": [[15, 15], [28, 208]], "include": []}, } assert len(mock_setup.mock_calls) == 1 @@ -920,11 +1135,11 @@ async def test_form_import_non_secure_device_discovered(hass: HomeAssistant) -> DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={ - "host": "elk://127.0.0.1:2101", - "username": "", - "password": "", - "auto_configure": True, - "prefix": "ohana", + CONF_HOST: "elk://127.0.0.1:2101", + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_AUTO_CONFIGURE: True, + CONF_PREFIX: "ohana", }, ) await hass.async_block_till_done() @@ -933,11 +1148,11 @@ async def test_form_import_non_secure_device_discovered(hass: HomeAssistant) -> assert result["title"] == "ohana" assert result["result"].unique_id == MOCK_MAC assert result["data"] == { - "auto_configure": True, - "host": "elk://127.0.0.1:2101", - "password": "", - "prefix": "ohana", - "username": "", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://127.0.0.1:2101", + CONF_PASSWORD: "", + CONF_PREFIX: "ohana", + CONF_USERNAME: "", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -964,11 +1179,11 @@ async def test_form_import_non_secure_non_stanadard_port_device_discovered( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={ - "host": "elk://127.0.0.1:444", - "username": "", - "password": "", - "auto_configure": True, - "prefix": "ohana", + CONF_HOST: "elk://127.0.0.1:444", + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_AUTO_CONFIGURE: True, + CONF_PREFIX: "ohana", }, ) await hass.async_block_till_done() @@ -977,11 +1192,11 @@ async def test_form_import_non_secure_non_stanadard_port_device_discovered( assert result["title"] == "ohana" assert result["result"].unique_id == MOCK_MAC assert result["data"] == { - "auto_configure": True, - "host": "elk://127.0.0.1:444", - "password": "", - "prefix": "ohana", - "username": "", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://127.0.0.1:444", + CONF_PASSWORD: "", + CONF_PREFIX: "ohana", + CONF_USERNAME: "", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -998,11 +1213,11 @@ async def test_form_import_non_secure_device_discovered_invalid_auth( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={ - "host": "elks://127.0.0.1", - "username": "invalid", - "password": "", - "auto_configure": False, - "prefix": "ohana", + CONF_HOST: "elks://127.0.0.1", + CONF_USERNAME: "invalid", + CONF_PASSWORD: "", + CONF_AUTO_CONFIGURE: False, + CONF_PREFIX: "ohana", }, ) await hass.async_block_till_done() @@ -1024,11 +1239,11 @@ async def test_form_import_existing(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={ - "host": f"elks://{MOCK_IP_ADDRESS}", - "username": "friend", - "password": "love", + CONF_HOST: f"elks://{MOCK_IP_ADDRESS}", + CONF_USERNAME: "friend", + CONF_PASSWORD: "love", "temperature_unit": "C", - "auto_configure": False, + CONF_AUTO_CONFIGURE: False, "keypad": { "enabled": True, "exclude": [], @@ -1037,7 +1252,7 @@ async def test_form_import_existing(hass: HomeAssistant) -> None: "output": {"enabled": False, "exclude": [], "include": []}, "counter": {"enabled": False, "exclude": [], "include": []}, "plc": {"enabled": False, "exclude": [], "include": []}, - "prefix": "ohana", + CONF_PREFIX: "ohana", "setting": {"enabled": False, "exclude": [], "include": []}, "area": {"enabled": False, "exclude": [], "include": []}, "task": {"enabled": False, "exclude": [], "include": []}, @@ -1183,8 +1398,8 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) await hass.async_block_till_done() @@ -1192,11 +1407,11 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { - "auto_configure": True, - "host": "elks://127.0.0.1", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://127.0.0.1", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -1233,8 +1448,8 @@ async def test_discovered_by_discovery_non_standard_port(hass: HomeAssistant) -> result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) await hass.async_block_till_done() @@ -1242,11 +1457,11 @@ async def test_discovered_by_discovery_non_standard_port(hass: HomeAssistant) -> assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { - "auto_configure": True, - "host": "elks://127.0.0.1:444", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://127.0.0.1:444", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -1304,8 +1519,8 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) await hass.async_block_till_done() @@ -1313,11 +1528,11 @@ async def test_discovered_by_dhcp_udp_responds(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { - "auto_configure": True, - "host": "elks://127.0.0.1", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://127.0.0.1", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -1354,7 +1569,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port( result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "non-secure", + CONF_PROTOCOL: "non-secure", }, ) await hass.async_block_till_done() @@ -1362,11 +1577,11 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port( assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { - "auto_configure": True, - "host": "elk://127.0.0.1", - "password": "", - "prefix": "", - "username": "", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://127.0.0.1", + CONF_PASSWORD: "", + CONF_PREFIX: "", + CONF_USERNAME: "", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -1408,18 +1623,18 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry( ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"username": "test-username", "password": "test-password"}, + {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "ElkM1 ddeeff" assert result2["data"] == { - "auto_configure": True, - "host": "elks://127.0.0.1", - "password": "test-password", - "prefix": "ddeeff", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://127.0.0.1", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "ddeeff", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 2 @@ -1477,8 +1692,8 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) await hass.async_block_till_done() @@ -1486,11 +1701,11 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { - "auto_configure": True, - "host": "elks://127.0.0.1", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://127.0.0.1", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -1526,8 +1741,8 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - "username": "test-username", - "password": "test-password", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) await hass.async_block_till_done() @@ -1535,11 +1750,11 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeefe" assert result3["data"] == { - "auto_configure": True, - "host": "elks://127.0.0.2", - "password": "test-password", - "prefix": "ddeefe", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elks://127.0.0.2", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "ddeefe", + CONF_USERNAME: "test-username", } assert len(mock_setup_entry.mock_calls) == 1 @@ -1568,9 +1783,9 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "non-secure", - "address": "1.2.3.4", - "prefix": "guest_house", + CONF_PROTOCOL: "non-secure", + CONF_ADDRESS: "1.2.3.4", + CONF_PREFIX: "guest_house", }, ) await hass.async_block_till_done() @@ -1578,11 +1793,11 @@ async def test_multiple_instances_with_discovery(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "guest_house" assert result2["data"] == { - "auto_configure": True, - "host": "elk://1.2.3.4", - "prefix": "guest_house", - "username": "", - "password": "", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://1.2.3.4", + CONF_PREFIX: "guest_house", + CONF_USERNAME: "", + CONF_PASSWORD: "", } assert len(mock_setup_entry.mock_calls) == 1 @@ -1629,9 +1844,9 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - "protocol": "TLS 1.2", - "username": "test-username", - "password": "test-password", + CONF_PROTOCOL: "TLS 1.2", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) await hass.async_block_till_done() @@ -1639,11 +1854,11 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeeff" assert result3["data"] == { - "auto_configure": True, - "host": "elksv1_2://127.0.0.1", - "password": "test-password", - "prefix": "", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elksv1_2://127.0.0.1", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "", + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -1679,9 +1894,9 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - "protocol": "TLS 1.2", - "username": "test-username", - "password": "test-password", + CONF_PROTOCOL: "TLS 1.2", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", }, ) await hass.async_block_till_done() @@ -1689,11 +1904,11 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "ElkM1 ddeefe" assert result3["data"] == { - "auto_configure": True, - "host": "elksv1_2://127.0.0.2", - "password": "test-password", - "prefix": "ddeefe", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elksv1_2://127.0.0.2", + CONF_PASSWORD: "test-password", + CONF_PREFIX: "ddeefe", + CONF_USERNAME: "test-username", } assert len(mock_setup_entry.mock_calls) == 1 @@ -1722,11 +1937,11 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "protocol": "TLS 1.2", - "address": "1.2.3.4", - "prefix": "guest_house", - "password": "test-password", - "username": "test-username", + CONF_PROTOCOL: "TLS 1.2", + CONF_ADDRESS: "1.2.3.4", + CONF_PREFIX: "guest_house", + CONF_PASSWORD: "test-password", + CONF_USERNAME: "test-username", }, ) await hass.async_block_till_done() @@ -1734,10 +1949,392 @@ async def test_multiple_instances_with_tls_v12(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "guest_house" assert result2["data"] == { - "auto_configure": True, - "host": "elksv1_2://1.2.3.4", - "prefix": "guest_house", - "password": "test-password", - "username": "test-username", + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elksv1_2://1.2.3.4", + CONF_PREFIX: "guest_house", + CONF_PASSWORD: "test-password", + CONF_USERNAME: "test-username", } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reconfigure_nonsecure( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reconfigure flow switching to non-secure protocol.""" + # Add mock_config_entry to hass before updating + mock_config_entry.add_to_hass(hass) + + # Update mock_config_entry.data using async_update_entry + hass.config_entries.async_update_entry( + mock_config_entry, + data={ + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://localhost", + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_PREFIX: "", + }, + ) + + await hass.async_block_till_done() + + result = await mock_config_entry.start_reconfigure_flow(hass) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + # Mock elk library to simulate successful connection + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_elk(mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROTOCOL: "non-secure", + CONF_ADDRESS: "1.2.3.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reconfigure_successful" + + # Verify the config entry was updated with the new data + assert dict(mock_config_entry.data) == { + CONF_AUTO_CONFIGURE: True, + CONF_HOST: "elk://1.2.3.4", + CONF_USERNAME: "", + CONF_PASSWORD: "", + CONF_PREFIX: "", + } + + # Verify the setup was called during reload + mock_setup_entry.assert_called_once() + + # Verify the elk library was initialized and connected + assert mocked_elk.connect.call_count == 1 + assert mocked_elk.disconnect.call_count == 1 + + +async def test_reconfigure_tls( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test reconfigure flow switching to TLS 1.2 protocol, validating host, username, and password update.""" + mock_config_entry.add_to_hass(hass) + await hass.async_block_till_done() + + result = await mock_config_entry.start_reconfigure_flow(hass) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_discovery(no_device=True), # ensure no UDP/DNS work + _patch_elk(mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: "127.0.0.1", + CONF_PROTOCOL: "TLS 1.2", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reconfigure_successful" + assert mock_config_entry.data[CONF_HOST] == "elksv1_2://127.0.0.1" + assert mock_config_entry.data[CONF_USERNAME] == "test-username" + assert mock_config_entry.data[CONF_PASSWORD] == "test-password" + mock_setup_entry.assert_called_once() + + +async def test_reconfigure_device_offline( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test reconfigure flow fails when device is offline.""" + mock_config_entry.add_to_hass(hass) + await hass.async_block_till_done() + + result = await mock_config_entry.start_reconfigure_flow(hass) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + mocked_elk = mock_elk(invalid_auth=None, sync_complete=None) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch("homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", 0), + patch("homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + # Retry with successful connection + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.ABORT + assert result3["reason"] == "reconfigure_successful" + mock_setup_entry.assert_called_once() + + +async def test_reconfigure_invalid_auth( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test reconfigure flow with invalid authentication.""" + mock_config_entry.add_to_hass(hass) + await hass.async_block_till_done() + + result = await mock_config_entry.start_reconfigure_flow(hass) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + elk = mock_elk(invalid_auth=True, sync_complete=True) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=elk), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "wronguser", + CONF_PASSWORD: "wrongpass", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} + + # Retry with correct auth + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "127.0.0.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.ABORT + assert result3["reason"] == "reconfigure_successful" + mock_setup_entry.assert_called_once() + + +async def test_reconfigure_different_device( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Abort reconfigure if the device unique_id differs.""" + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry(mock_config_entry, unique_id=MOCK_MAC) + await hass.async_block_till_done() + + result = await mock_config_entry.start_reconfigure_flow(hass) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + different_device = ElkSystem("bb:cc:dd:ee:ff:aa", "1.2.3.4", 2601) + elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(device=different_device), _patch_elk(elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test", + CONF_PASSWORD: "test", + }, + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Abort occurs when the discovered device's unique_id does not match the existing config entry. + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "unique_id_mismatch" + + +async def test_reconfigure_unknown_error( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test reconfigure flow with an unexpected exception.""" + mock_config_entry.add_to_hass(hass) + await hass.async_block_till_done() + + result = await mock_config_entry.start_reconfigure_flow(hass) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + elk = mock_elk(invalid_auth=None, sync_complete=None, exception=OSError) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=elk), + patch("homeassistant.components.elkm1.config_flow.VALIDATE_TIMEOUT", 0), + patch("homeassistant.components.elkm1.config_flow.LOGIN_TIMEOUT", 0), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "1.2.3.4", + CONF_USERNAME: "test", + CONF_PASSWORD: "test", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + # Retry with successful connection + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with ( + _patch_discovery(no_device=True), + _patch_elk(elk=mocked_elk), + patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PROTOCOL: "secure", + CONF_ADDRESS: "127.0.0.1", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] is FlowResultType.ABORT + assert result3["reason"] == "reconfigure_successful" + mock_setup_entry.assert_called_once() + + +async def test_reconfigure_preserves_existing_config_entry_fields( + hass: HomeAssistant, +) -> None: + """Test reconfigure only updates changed fields and preserves existing config entry data.""" + # Simulate a config entry imported from yaml with extra fields + initial_data = { + CONF_HOST: "elks://1.2.3.4", + CONF_USERNAME: "olduser", + CONF_PASSWORD: "oldpass", + CONF_PREFIX: "oldprefix", + CONF_AUTO_CONFIGURE: False, + "extra_field": "should_be_preserved", + "another_field": 42, + } + config_entry = MockConfigEntry( + domain=DOMAIN, + data=initial_data, + unique_id=MOCK_MAC, + ) + config_entry.add_to_hass(hass) + await hass.async_block_till_done() + + result = await config_entry.start_reconfigure_flow(hass) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reconfigure" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + with ( + _patch_discovery(no_device=True), + _patch_elk(mocked_elk), + patch("homeassistant.components.elkm1.async_setup_entry", return_value=True), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "newuser", + CONF_PASSWORD: "newpass", + CONF_ADDRESS: "5.6.7.8", + CONF_PROTOCOL: "secure", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.ABORT + assert result2["reason"] == "reconfigure_successful" + + await hass.async_block_till_done() + updated_entry = hass.config_entries.async_get_entry(config_entry.entry_id) + assert updated_entry is not None + assert updated_entry.data[CONF_HOST] == "elks://5.6.7.8" + assert updated_entry.data[CONF_USERNAME] == "newuser" + assert updated_entry.data[CONF_PASSWORD] == "newpass" + assert updated_entry.data[CONF_AUTO_CONFIGURE] is False + assert updated_entry.data[CONF_PREFIX] == "oldprefix" + assert updated_entry.data["extra_field"] == "should_be_preserved" + assert updated_entry.data["another_field"] == 42 diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 255e2e9a017e63..dca29cd7abd632 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -121,7 +121,6 @@ async def integration_fixture( "smoke_detector", "solar_power", "switch_unit", - "switchbot_K11", "temperature_sensor", "thermostat", "vacuum_cleaner", diff --git a/tests/components/matter/fixtures/nodes/switchbot_K11.json b/tests/components/matter/fixtures/nodes/switchbot_K11.json deleted file mode 100644 index 615979117e0f8a..00000000000000 --- a/tests/components/matter/fixtures/nodes/switchbot_K11.json +++ /dev/null @@ -1,440 +0,0 @@ -{ - "node_id": 97, - "date_commissioned": "2025-08-21T16:38:31.165712", - "last_interview": "2025-08-21T16:38:31.165730", - "interview_version": 6, - "available": true, - "is_bridge": false, - "attributes": { - "0/29/0": [ - { - "0": 22, - "1": 1 - } - ], - "0/29/1": [29, 31, 40, 48, 51, 60, 62, 63], - "0/29/2": [], - "0/29/3": [1], - "0/29/65532": 0, - "0/29/65533": 2, - "0/29/65528": [], - "0/29/65529": [], - "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], - "0/31/0": [ - { - "1": 5, - "2": 2, - "3": [112233], - "4": null, - "254": 3 - } - ], - "0/31/1": [], - "0/31/2": 4, - "0/31/3": 3, - "0/31/4": 4, - "0/31/65532": 1, - "0/31/65533": 2, - "0/31/65528": [], - "0/31/65529": [], - "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], - "0/40/0": 18, - "0/40/1": "SwitchBot", - "0/40/2": 5015, - "0/40/3": "K11+", - "0/40/4": 2043, - "0/40/5": "", - "0/40/6": "**REDACTED**", - "0/40/7": 8, - "0/40/8": "8", - "0/40/9": 2, - "0/40/10": "2.0", - "0/40/11": "20200101", - "0/40/15": "SY612505261610300E", - "0/40/16": false, - "0/40/18": "5E441F48C89E75F4", - "0/40/19": { - "0": 3, - "1": 65535 - }, - "0/40/21": 17039616, - "0/40/22": 1, - "0/40/65532": 0, - "0/40/65533": 4, - "0/40/65528": [], - "0/40/65529": [], - "0/40/65531": [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 15, 16, 18, 19, 21, 22, 65528, - 65529, 65531, 65532, 65533 - ], - "0/48/0": 0, - "0/48/1": { - "0": 60, - "1": 900 - }, - "0/48/2": 0, - "0/48/3": 2, - "0/48/4": true, - "0/48/65532": 0, - "0/48/65533": 2, - "0/48/65528": [1, 3, 5], - "0/48/65529": [0, 2, 4], - "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], - "0/51/0": [ - { - "0": "wlan0", - "1": true, - "2": null, - "3": null, - "4": "sOn+hWUk", - "5": ["wKgBow=="], - "6": ["KgEOCgKzOZBGmN+UianfsA==", "/oAAAAAAAACEb4xWVmm9jw=="], - "7": 1 - }, - { - "0": "lo", - "1": true, - "2": null, - "3": null, - "4": "AAAAAAAA", - "5": ["fwAAAQ=="], - "6": ["AAAAAAAAAAAAAAAAAAAAAQ=="], - "7": 0 - } - ], - "0/51/1": 8, - "0/51/2": 0, - "0/51/4": 0, - "0/51/5": [], - "0/51/6": [], - "0/51/7": [], - "0/51/8": false, - "0/51/65532": 0, - "0/51/65533": 2, - "0/51/65528": [2], - "0/51/65529": [0, 1], - "0/51/65531": [0, 1, 2, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533], - "0/60/0": 0, - "0/60/1": null, - "0/60/2": null, - "0/60/65532": 0, - "0/60/65533": 1, - "0/60/65528": [], - "0/60/65529": [0, 2], - "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], - "0/62/0": [ - { - "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRYRgkBwEkCAEwCUEED3gG83T4fgQ8mJi4UtxYHdce62io4H76mdpHCQluYUJ3zb4ahgxgT9tz7eNDwOooSPo985+iv5hDEEYsuVUu1TcKNQEoARgkAgE2AwQCBAEYMAQUGDYBbm6GdsqVhw7HwYXe2fWNMXIwBRS5+zzv8ZPGnI9mC3wH9vq10JnwlhgwC0DuruGO/yh7HLCuMeBxe6kBbjeStJ+VJAdWHiXBEyE1x2LZPcgX1LXpIwjshY5ACCNFRTuwtIH9GwSt9iVKZc7/GA==", - "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEE/DujEcdTsX19xbxX+KuKKWiMaA5D9u99P/pVxIOmscd2BA2PadEMNnjvtPOpf+WE2Zxar4rby1IfAClGUUuQrTcKNQEpARgkAmAwBBS5+zzv8ZPGnI9mC3wH9vq10JnwljAFFPT6p93JKGcb7g+rTWnA6evF2EdGGDALQGkPpvsbkAFEbfPN6H3Kf23R0zzmW/gpAA3kgaL6wKB2Ofm+Tmylw22qM536Kj8mOMwaV0EL1dCCGcuxF98aL6gY", - "254": 3 - } - ], - "0/62/1": [ - { - "1": "***********", - "2": 4939, - "3": 2, - "4": 97, - "5": "SSID", - "254": 3 - } - ], - "0/62/2": 16, - "0/62/3": 5, - "0/62/4": ["***********"], - "0/62/5": 3, - "0/62/65532": 0, - "0/62/65533": 1, - "0/62/65528": [1, 3, 5, 8], - "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], - "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], - "0/63/0": [], - "0/63/1": [], - "0/63/2": 4, - "0/63/3": 3, - "0/63/65532": 0, - "0/63/65533": 2, - "0/63/65528": [2, 5], - "0/63/65529": [0, 1, 3, 4], - "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], - "1/3/0": 0, - "1/3/1": 0, - "1/3/65532": 0, - "1/3/65533": 5, - "1/3/65528": [], - "1/3/65529": [0], - "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], - "1/29/0": [ - { - "0": 116, - "1": 1 - } - ], - "1/29/1": [3, 29, 84, 85, 97, 336], - "1/29/2": [], - "1/29/3": [], - "1/29/65532": 0, - "1/29/65533": 2, - "1/29/65528": [], - "1/29/65529": [], - "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], - "1/84/0": [ - { - "0": "Idle", - "1": 0, - "2": [ - { - "1": 16384 - } - ] - }, - { - "0": "Cleaning", - "1": 1, - "2": [ - { - "1": 16385 - } - ] - }, - { - "0": "Mapping", - "1": 2, - "2": [ - { - "1": 16386 - } - ] - }, - { - "0": "Pause", - "1": 3, - "2": [ - { - "1": 32769 - }, - { - "1": 0 - } - ] - }, - { - "0": "Resume", - "1": 4, - "2": [ - { - "1": 32770 - }, - { - "1": 0 - } - ] - }, - { - "0": "Docking", - "1": 5, - "2": [ - { - "1": 32771 - }, - { - "1": 0 - } - ] - } - ], - "1/84/1": 0, - "1/84/65532": 0, - "1/84/65533": 3, - "1/84/65528": [1], - "1/84/65529": [0], - "1/84/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], - "1/85/0": [ - { - "0": "Quick", - "1": 0, - "2": [ - { - "1": 16385 - }, - { - "1": 1 - } - ] - }, - { - "0": "Auto", - "1": 1, - "2": [ - { - "1": 16385 - }, - { - "1": 0 - } - ] - }, - { - "0": "Deep Clean", - "1": 2, - "2": [ - { - "1": 16385 - }, - { - "1": 16384 - } - ] - }, - { - "0": "Quiet", - "1": 3, - "2": [ - { - "1": 16385 - }, - { - "1": 2 - } - ] - }, - { - "0": "Max Vac", - "1": 4, - "2": [ - { - "1": 16385 - }, - { - "1": 7 - } - ] - } - ], - "1/85/1": 0, - "1/85/65532": 0, - "1/85/65533": 3, - "1/85/65528": [1], - "1/85/65529": [0], - "1/85/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], - "1/97/0": null, - "1/97/1": null, - "1/97/3": [ - { - "0": 0 - }, - { - "0": 1 - }, - { - "0": 2 - }, - { - "0": 3 - }, - { - "0": 64 - }, - { - "0": 65 - }, - { - "0": 66 - } - ], - "1/97/4": 0, - "1/97/5": { - "0": 0 - }, - "1/97/65532": 0, - "1/97/65533": 2, - "1/97/65528": [4], - "1/97/65529": [0, 3, 128], - "1/97/65531": [0, 1, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], - "1/336/0": [ - { - "0": 1, - "1": null, - "2": { - "0": { - "0": "Bedroom #3", - "1": null, - "2": null - }, - "1": null - } - }, - { - "0": 2, - "1": null, - "2": { - "0": { - "0": "Stairs", - "1": null, - "2": null - }, - "1": null - } - }, - { - "0": 3, - "1": null, - "2": { - "0": { - "0": "Bedroom #1", - "1": null, - "2": null - }, - "1": null - } - }, - { - "0": 4, - "1": null, - "2": { - "0": { - "0": "Bedroom #2", - "1": null, - "2": null - }, - "1": null - } - }, - { - "0": 5, - "1": null, - "2": { - "0": { - "0": "Corridor", - "1": null, - "2": null - }, - "1": null - } - }, - { - "0": 6, - "1": null, - "2": { - "0": { - "0": "Bathroom", - "1": null, - "2": null - }, - "1": null - } - } - ], - "1/336/1": [], - "1/336/2": [4, 3], - "1/336/3": null, - "1/336/4": null, - "1/336/5": [], - "1/336/65532": 6, - "1/336/65533": 1, - "1/336/65528": [1, 3], - "1/336/65529": [0, 2], - "1/336/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533] - }, - "attribute_subscriptions": [] -} diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index 99228281971d32..aab3d5f7ccec35 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -2676,69 +2676,6 @@ 'state': 'previous', }) # --- -# name: test_selects[switchbot_K11][select.k11_clean_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'Quick', - 'Auto', - 'Deep Clean', - 'Quiet', - 'Max Vac', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'select', - 'entity_category': None, - 'entity_id': 'select.k11_clean_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Clean mode', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'clean_mode', - 'unique_id': '00000000000004D2-0000000000000061-MatterNodeDevice-1-MatterRvcCleanMode-85-1', - 'unit_of_measurement': None, - }) -# --- -# name: test_selects[switchbot_K11][select.k11_clean_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'K11+ Clean mode', - 'options': list([ - 'Quick', - 'Auto', - 'Deep Clean', - 'Quiet', - 'Max Vac', - ]), - }), - 'context': , - 'entity_id': 'select.k11_clean_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'Quick', - }) -# --- # name: test_selects[thermostat][select.longan_link_hvac_temperature_display_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_sensor.ambr b/tests/components/matter/snapshots/test_sensor.ambr index 09be27dcc15d5c..2567ce2e936b78 100644 --- a/tests/components/matter/snapshots/test_sensor.ambr +++ b/tests/components/matter/snapshots/test_sensor.ambr @@ -6791,74 +6791,6 @@ 'state': '234.899', }) # --- -# name: test_sensors[switchbot_K11][sensor.k11_operational_state-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'stopped', - 'running', - 'paused', - 'error', - 'seeking_charger', - 'charging', - 'docked', - ]), - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.k11_operational_state', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Operational state', - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'operational_state', - 'unique_id': '00000000000004D2-0000000000000061-MatterNodeDevice-1-RvcOperationalState-97-4', - 'unit_of_measurement': None, - }) -# --- -# name: test_sensors[switchbot_K11][sensor.k11_operational_state-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'K11+ Operational state', - 'options': list([ - 'stopped', - 'running', - 'paused', - 'error', - 'seeking_charger', - 'charging', - 'docked', - ]), - }), - 'context': , - 'entity_id': 'sensor.k11_operational_state', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'stopped', - }) -# --- # name: test_sensors[temperature_sensor][sensor.mock_temperature_sensor_temperature-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/snapshots/test_vacuum.ambr b/tests/components/matter/snapshots/test_vacuum.ambr index 78d90b00dcdfd9..71e0f75614ddd8 100644 --- a/tests/components/matter/snapshots/test_vacuum.ambr +++ b/tests/components/matter/snapshots/test_vacuum.ambr @@ -1,59 +1,4 @@ # serializer version: 1 -# name: test_vacuum[switchbot_K11][vacuum.k11-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'vacuum', - 'entity_category': None, - 'entity_id': 'vacuum.k11', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': None, - 'platform': 'matter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': , - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000061-MatterNodeDevice-1-MatterVacuumCleaner-84-1', - 'unit_of_measurement': None, - }) -# --- -# name: test_vacuum[switchbot_K11][vacuum.k11-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'current_area': None, - 'current_area_name': None, - 'friendly_name': 'K11+', - 'selected_areas': list([ - 4, - 3, - ]), - 'supported_features': , - }), - 'context': , - 'entity_id': 'vacuum.k11', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'idle', - }) -# --- # name: test_vacuum[vacuum_cleaner][vacuum.mock_vacuum-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -92,10 +37,7 @@ # name: test_vacuum[vacuum_cleaner][vacuum.mock_vacuum-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'current_area': 7, - 'current_area_name': 'My Location A', 'friendly_name': 'Mock Vacuum', - 'selected_areas': None, 'supported_features': , }), 'context': , diff --git a/tests/components/matter/test_vacuum.py b/tests/components/matter/test_vacuum.py index 36a4b5275df731..cba4b9b59ebf10 100644 --- a/tests/components/matter/test_vacuum.py +++ b/tests/components/matter/test_vacuum.py @@ -7,16 +7,6 @@ import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components.matter.const import ( - SERVICE_CLEAN_AREAS, - SERVICE_GET_AREAS, - SERVICE_SELECT_AREAS, -) -from homeassistant.components.matter.vacuum import ( - ATTR_CURRENT_AREA, - ATTR_CURRENT_AREA_NAME, - ATTR_SELECTED_AREAS, -) from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -147,162 +137,6 @@ async def test_vacuum_actions( matter_client.send_device_command.reset_mock() -@pytest.mark.parametrize("node_fixture", ["switchbot_K11"]) -async def test_k11_vacuum_actions( - hass: HomeAssistant, - matter_client: MagicMock, - matter_node: MatterNode, -) -> None: - """Test Matter ServiceArea cluster actions.""" - # Fetch translations - await async_setup_component(hass, "homeassistant", {}) - entity_id = "vacuum.k11" - state = hass.states.get(entity_id) - # test selected_areas action - assert state - - selected_areas = [1, 2, 3] - await hass.services.async_call( - "matter", - SERVICE_SELECT_AREAS, - { - "entity_id": entity_id, - "areas": selected_areas, - }, - blocking=True, - return_response=True, - ) - assert matter_client.send_device_command.call_count == 1 - assert matter_client.send_device_command.call_args == call( - node_id=matter_node.node_id, - endpoint_id=1, - command=clusters.ServiceArea.Commands.SelectAreas(newAreas=selected_areas), - ) - matter_client.send_device_command.reset_mock() - - # test clean_areasss action - assert state - - selected_areas = [1, 2, 3] - await hass.services.async_call( - "matter", - SERVICE_CLEAN_AREAS, - { - "entity_id": entity_id, - "areas": selected_areas, - }, - blocking=True, - return_response=True, - ) - assert matter_client.send_device_command.call_count == 2 - assert matter_client.send_device_command.call_args_list[0] == call( - node_id=matter_node.node_id, - endpoint_id=1, - command=clusters.ServiceArea.Commands.SelectAreas(newAreas=selected_areas), - ) - assert matter_client.send_device_command.call_args_list[1] == call( - node_id=matter_node.node_id, - endpoint_id=1, - command=clusters.RvcRunMode.Commands.ChangeToMode(newMode=1), - ) - matter_client.send_device_command.reset_mock() - - # test get_areas action - response = await hass.services.async_call( - "matter", - SERVICE_GET_AREAS, - { - "entity_id": entity_id, - }, - blocking=True, - return_response=True, - ) - # check the response data - expected_data = { - "vacuum.k11": { - "areas": { - 1: {"name": "Bedroom #3"}, - 2: {"name": "Stairs"}, - 3: {"name": "Bedroom #1"}, - 4: {"name": "Bedroom #2"}, - 5: {"name": "Corridor"}, - 6: {"name": "Bathroom"}, - }, - "maps": [], - } - } - assert response == expected_data - - -@pytest.mark.parametrize("node_fixture", ["switchbot_K11"]) -async def test_k11_vacuum_service_area( - hass: HomeAssistant, - matter_client: MagicMock, - matter_node: MatterNode, -) -> None: - """Test Matter ServiceArea cluster attributes.""" - # Fetch translations - await async_setup_component(hass, "homeassistant", {}) - entity_id = "vacuum.k11" - state = hass.states.get(entity_id) - # SupportedAreas attribute ID is 2 (1/336/0) - supported_areas = [ - { - "0": 1, - "1": None, - "2": { - "0": { - "0": "Bedroom #1", - "1": None, - "2": None, - }, - "1": None, - }, - }, - { - "0": 3, - "1": None, - "2": { - "0": { - "0": "Bedroom #2", - "1": None, - "2": None, - }, - "1": None, - }, - }, - { - "0": 4, - "1": None, - "2": { - "0": { - "0": "Bedroom #3", - "1": None, - "2": None, - }, - "1": None, - }, - }, - ] - set_node_attribute(matter_node, 1, 336, 0, supported_areas) - await trigger_subscription_callback(hass, matter_client) - state = hass.states.get(entity_id) - assert state - - selected_areas = [1, 3] - set_node_attribute(matter_node, 1, 336, 2, selected_areas) - await trigger_subscription_callback(hass, matter_client) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_SELECTED_AREAS] == selected_areas - - # ServiceArea.Attributes.CurrentArea (1/336/3) - set_node_attribute(matter_node, 1, 336, 3, 4) - await trigger_subscription_callback(hass, matter_client) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_CURRENT_AREA] == 4 - assert state.attributes[ATTR_CURRENT_AREA_NAME] == "Bedroom #3" - - @pytest.mark.parametrize("node_fixture", ["vacuum_cleaner"]) async def test_vacuum_updates( hass: HomeAssistant, diff --git a/tests/components/nederlandse_spoorwegen/__init__.py b/tests/components/nederlandse_spoorwegen/__init__.py index a6b27df6185ffa..da2803d9efb918 100644 --- a/tests/components/nederlandse_spoorwegen/__init__.py +++ b/tests/components/nederlandse_spoorwegen/__init__.py @@ -1 +1,13 @@ """Tests for the Nederlandse Spoorwegen integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Set up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/nederlandse_spoorwegen/conftest.py b/tests/components/nederlandse_spoorwegen/conftest.py index 6e58a2e483eff6..c2bcdfedd87b0a 100644 --- a/tests/components/nederlandse_spoorwegen/conftest.py +++ b/tests/components/nederlandse_spoorwegen/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch -from ns_api import Station +from ns_api import Station, Trip import pytest from homeassistant.components.nederlandse_spoorwegen.const import ( @@ -33,15 +33,23 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture def mock_nsapi() -> Generator[AsyncMock]: """Override async_setup_entry.""" - with patch( - "homeassistant.components.nederlandse_spoorwegen.config_flow.NSAPI", - autospec=True, - ) as mock_nsapi: + with ( + patch( + "homeassistant.components.nederlandse_spoorwegen.config_flow.NSAPI", + autospec=True, + ) as mock_nsapi, + patch( + "homeassistant.components.nederlandse_spoorwegen.NSAPI", + new=mock_nsapi, + ), + ): client = mock_nsapi.return_value stations = load_json_object_fixture("stations.json", DOMAIN) client.get_stations.return_value = [ Station(station) for station in stations["payload"] ] + trips = load_json_object_fixture("trip.json", DOMAIN) + client.get_trips.return_value = [Trip(trip) for trip in trips["trips"]] yield client diff --git a/tests/components/nederlandse_spoorwegen/fixtures/trip.json b/tests/components/nederlandse_spoorwegen/fixtures/trip.json new file mode 100644 index 00000000000000..e79c15236788d9 --- /dev/null +++ b/tests/components/nederlandse_spoorwegen/fixtures/trip.json @@ -0,0 +1,7096 @@ +{ + "source": "HARP", + "trips": [ + { + "idx": 0, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T16:24:00+02:00|plannedArrivalTime=2025-09-15T18: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-15T16:24:00+02:00|plannedArrivalTime=2025-09-15T18:40:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=2333456918", + "sourceCtxRecon": "¶HKI¶T$A=1@O=Amsterdam Centraal@L=1100850@a=128@$A=1@O=Utrecht Centraal@L=1100728@a=128@$202509151624$202509151651$IC 3059 $$1$$$$$$§W$A=1@O=Utrecht Centraal@L=1100728@a=128@$A=1@O=Utrecht Centraal@L=1100905@a=128@$202509151651$202509151653$$$1$$$$$$§T$A=1@O=Utrecht Centraal@L=1100905@a=128@$A=1@O='s-Hertogenbosch@L=1100870@a=128@$202509151654$202509151722$IC 3559 $$3$$$$$$§W$A=1@O='s-Hertogenbosch@L=1100870@a=128@$A=1@O='s-Hertogenbosch@L=1101032@a=128@$202509151722$202509151726$$$1$$$$$$§T$A=1@O='s-Hertogenbosch@L=1101032@a=128@$A=1@O=Utrecht Centraal@L=1100715@a=128@$202509151730$202509151757$IC 2760 $$3$$$$$$§W$A=1@O=Utrecht Centraal@L=1100715@a=128@$A=1@O=Utrecht Centraal@L=1100930@a=128@$202509151757$202509151802$$$1$$$$$$§T$A=1@O=Utrecht Centraal@L=1100930@a=128@$A=1@O=Rotterdam Centraal@L=1100690@a=128@$202509151803$202509151840$IC 2860 $$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#¶KCC¶#VE#0#ERG#45319#HIN#460#ECK#13954|13944|14077|14080|0|0|485|13938|1|0|8|0|0|-2147483648#¶KRCC¶#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-15T16:24:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T16: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-15T16:24:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16: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-15T16: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-15T18:03:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18: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-15T18:40:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18: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-15T18:03:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18: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-15T18:22:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:22:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T18:21:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18: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-15T18:31:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:31:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T18:31:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18: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-15T18:40:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18: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-15T18: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-15T16:34:00+02:00|plannedArrivalTime=2025-09-15T18: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-15T16:34:00+02:00|plannedArrivalTime=2025-09-15T18:37:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1180343963", + "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=1100726@a=128@$202509151814$202509151837$ICD 1871$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#¶KCC¶#VE#0#ERG#261#HIN#390#ECK#13954|13954|14077|14077|0|0|66021|13938|2|0|2|0|0|-2147483648#¶KRCC¶#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-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": "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-15T18:14:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18: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-15T18:37:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18: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-15T18:14:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18: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-15T18:37:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18: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-15T18: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-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" + } + ] + }, + { + "idx": 3, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T16:46:00+02:00|plannedArrivalTime=2025-09-15T19:10:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=2212456600", + "ctxRecon": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T16:46:00+02:00|plannedArrivalTime=2025-09-15T19:10:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=2212456600", + "sourceCtxRecon": "¶HKI¶T$A=1@O=Amsterdam Centraal@L=1100850@a=128@$A=1@O='s-Hertogenbosch@L=1100870@a=128@$202509151646$202509151742$IC 3961 $$1$$$$$$§W$A=1@O='s-Hertogenbosch@L=1100870@a=128@$A=1@O='s-Hertogenbosch@L=1101032@a=128@$202509151742$202509151746$$$1$$$$$$§T$A=1@O='s-Hertogenbosch@L=1101032@a=128@$A=1@O=Utrecht Centraal@L=1100715@a=128@$202509151750$202509151816$IC 3962 $$1$$$$$$§W$A=1@O=Utrecht Centraal@L=1100715@a=128@$A=1@O=Utrecht Centraal@L=1100672@a=128@$202509151816$202509151821$$$1$$$$$$§T$A=1@O=Utrecht Centraal@L=1100672@a=128@$A=1@O=Rotterdam Centraal@L=1100690@a=128@$202509151833$202509151910$IC 2862 $$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#¶KCC¶#VE#0#ERG#45317#HIN#460#ECK#13985|13966|14107|14110|0|0|66021|13955|4|0|8|0|0|-2147483648#¶KRCC¶#VE#1#MRTF#", + "plannedDurationInMinutes": 144, + "actualDurationInMinutes": 144, + "transfers": 2, + "status": "NORMAL", + "messages": [], + "legs": [ + { + "idx": "0", + "name": "IC 3961", + "travelType": "PUBLIC_TRANSIT", + "direction": "Heerlen", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#445919#TA#0#DA#150925#1S#1101046#1T#1539#LS#1101030#LT#1911#PU#784#RT#1#CA#IC#ZE#3961#ZB#IC 3961 #PC#1#FR#1101046#FT#1539#TO#1101030#TT#1911#", + "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:46:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T16:46:00+0200", + "plannedTrack": "4b", + "actualTrack": "4b", + "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:42:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:42:00+0200", + "plannedTrack": "6", + "actualTrack": "6", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3961", + "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 Heerlen", + "shortValue": "richting Heerlen", + "accessibilityValue": "richting Heerlen", + "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:46:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16:46:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "4b", + "plannedDepartureTrack": "4b", + "plannedArrivalTrack": "4b", + "actualArrivalTrack": "4b", + "departureDelayInSeconds": 0, + "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:53:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16:53:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T16:53:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T16:53:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "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:14:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:14:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:12:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:12:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "18", + "plannedDepartureTrack": "18", + "plannedArrivalTrack": "18", + "actualArrivalTrack": "18", + "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:42:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:42:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "6", + "plannedDepartureTrack": "6", + "plannedArrivalTrack": "6", + "actualArrivalTrack": "6", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 6, + "punctuality": 66.7, + "crossPlatformTransfer": false, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#445919#TA#0#DA#150925#1S#1101046#1T#1539#LS#1101030#LT#1911#PU#784#RT#1#CA#IC#ZE#3961#ZB#IC 3961 #PC#1#FR#1101046#FT#1539#TO#1101030#TT#1911#&train=3961&datetime=2025-09-15T16:46:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 56, + "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": 4, + "distanceInMeters": 84795 + }, + { + "idx": "1", + "name": "IC 3962", + "travelType": "PUBLIC_TRANSIT", + "direction": "Enkhuizen", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#3719#TA#0#DA#150925#1S#1101030#1T#1619#LS#1101046#LT#1953#PU#784#RT#1#CA#IC#ZE#3962#ZB#IC 3962 #PC#1#FR#1101030#FT#1619#TO#1101046#TT#1953#", + "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:50:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:50: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-15T18:16:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:16:00+0200", + "plannedTrack": "7", + "actualTrack": "7", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3962", + "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 Enkhuizen", + "shortValue": "richting Enkhuizen", + "accessibilityValue": "richting Enkhuizen", + "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:50:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:50: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-15T18:16:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:16:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "7", + "plannedDepartureTrack": "7", + "plannedArrivalTrack": "7", + "actualArrivalTrack": "7", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "MEDIUM", + "bicycleSpotCount": 6, + "punctuality": 100.0, + "crossPlatformTransfer": false, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#3719#TA#0#DA#150925#1S#1101030#1T#1619#LS#1101046#LT#1953#PU#784#RT#1#CA#IC#ZE#3962#ZB#IC 3962 #PC#1#FR#1101030#FT#1619#TO#1101046#TT#1953#&train=3962&datetime=2025-09-15T17:50:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 26, + "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": 5, + "distanceInMeters": 47266 + }, + { + "idx": "2", + "name": "IC 2862", + "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#1137#TA#2#DA#150925#1S#1100672#1T#1833#LS#1100690#LT#1910#PU#784#RT#1#CA#IC#ZE#2862#ZB#IC 2862 #PC#1#FR#1100672#FT#1833#TO#1100690#TT#1910#", + "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-15T18:33:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:33:00+0200", + "plannedTrack": "11", + "actualTrack": "11", + "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-15T19:10:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T19:10:00+0200", + "plannedTrack": "14", + "actualTrack": "14", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "2862", + "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": "17 min. overstaptijd", + "accessibilityMessage": "17 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-15T18:33:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:33:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "11", + "plannedDepartureTrack": "11", + "plannedArrivalTrack": "11", + "actualArrivalTrack": "11", + "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-15T18:52:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:52:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T18:51:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:51: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-15T19:01:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T19:01:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T19:01:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T19:01: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-15T19:10:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T19:10:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "14", + "plannedDepartureTrack": "14", + "plannedArrivalTrack": "14", + "actualArrivalTrack": "14", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 6, + "punctuality": 88.9, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1137#TA#2#DA#150925#1S#1100672#1T#1833#LS#1100690#LT#1910#PU#784#RT#1#CA#IC#ZE#2862#ZB#IC 2862 #PC#1#FR#1100672#FT#1833#TO#1100690#TT#1910#&train=2862&datetime=2025-09-15T18:33: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": "83293275_3", + "crowdForecast": "MEDIUM", + "punctuality": 66.7, + "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/1646/1910?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%3A46%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A10%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D2212456600" + }, + "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%3A46%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A10%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D2212456600&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": "3", + "accessibilityName": "Intercity" + }, + { + "name": "Intercity", + "nameNesProperties": { + "color": "text-subtle", + "styles": { + "type": "TextStyles", + "strikethrough": false, + "bold": false + } + }, + "iconNesProperties": { + "color": "text-body", + "icon": "train" + }, + "actualTrack": "11", + "accessibilityName": "Intercity" + } + ] + }, + { + "idx": 4, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T16:46:00+02:00|plannedArrivalTime=2025-09-15T19:15:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=3511802831", + "ctxRecon": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T16:46:00+02:00|plannedArrivalTime=2025-09-15T19:15:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=3511802831", + "sourceCtxRecon": "¶HKI¶T$A=1@O=Amsterdam Centraal@L=1100850@a=128@$A=1@O=Eindhoven Centraal@L=1100884@a=128@$202509151646$202509151803$IC 3961 $$1$$$$$$§W$A=1@O=Eindhoven Centraal@L=1100884@a=128@$A=1@O=Eindhoven Centraal@L=1100921@a=128@$202509151803$202509151808$$$1$$$$$$§T$A=1@O=Eindhoven Centraal@L=1100921@a=128@$A=1@O=Rotterdam Centraal@L=1100726@a=128@$202509151814$202509151915$IC 1164 $$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#¶KCC¶#VE#0#ERG#8451#HIN#460#ECK#13985|13966|14107|14115|0|0|485|13955|5|0|10|0|0|-2147483648#¶KRCC¶#VE#1#MRTF#", + "plannedDurationInMinutes": 149, + "actualDurationInMinutes": 149, + "transfers": 1, + "status": "NORMAL", + "primaryMessage": { + "title": "Kortere trein, extra druk", + "nesProperties": { + "color": "text-warning-contrast", + "type": "warning", + "icon": "alert" + }, + "type": "SHORTENED_TRAIN" + }, + "messages": [], + "legs": [ + { + "idx": "0", + "name": "IC 3961", + "travelType": "PUBLIC_TRANSIT", + "direction": "Heerlen", + "partCancelled": false, + "cancelled": false, + "isAfterCancelledLeg": false, + "isOnOrAfterCancelledLeg": false, + "changePossible": true, + "alternativeTransport": false, + "journeyDetailRef": "HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#445919#TA#0#DA#150925#1S#1101046#1T#1539#LS#1101030#LT#1911#PU#784#RT#1#CA#IC#ZE#3961#ZB#IC 3961 #PC#1#FR#1101046#FT#1539#TO#1101030#TT#1911#", + "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:46:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T16:46:00+0200", + "plannedTrack": "4b", + "actualTrack": "4b", + "checkinStatus": "NOTHING", + "notes": [] + }, + "destination": { + "name": "Eindhoven Centraal", + "lng": 5.48138904571533, + "lat": 51.4433326721191, + "countryCode": "NL", + "uicCode": "8400206", + "uicCdCode": "118400206", + "stationCode": "EHV", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T18:03:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:03:00+0200", + "plannedTrack": "2", + "actualTrack": "2", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3961", + "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 Heerlen", + "shortValue": "richting Heerlen", + "accessibilityValue": "richting Heerlen", + "key": "PRODUCT_DIRECTION", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ], + [ + { + "value": "3 tussenstops", + "shortValue": "3 tussenstops", + "accessibilityValue": "3 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:46:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16:46:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "4b", + "plannedDepartureTrack": "4b", + "plannedArrivalTrack": "4b", + "actualArrivalTrack": "4b", + "departureDelayInSeconds": 0, + "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:53:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16:53:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T16:53:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T16:53:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "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:14:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:14:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:12:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:12:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "18", + "plannedDepartureTrack": "18", + "plannedArrivalTrack": "18", + "actualArrivalTrack": "18", + "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, + "plannedDepartureDateTime": "2025-09-15T17:45:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:45:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:42:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:42:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "6", + "plannedDepartureTrack": "6", + "plannedArrivalTrack": "6", + "actualArrivalTrack": "6", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400206", + "uicCdCode": "118400206", + "name": "Eindhoven Centraal", + "lat": 51.4433326721191, + "lng": 5.48138904571533, + "countryCode": "NL", + "notes": [], + "routeIdx": 23, + "plannedArrivalDateTime": "2025-09-15T18:03:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:03:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "2", + "plannedDepartureTrack": "2", + "plannedArrivalTrack": "2", + "actualArrivalTrack": "2", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "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#445919#TA#0#DA#150925#1S#1101046#1T#1539#LS#1101030#LT#1911#PU#784#RT#1#CA#IC#ZE#3961#ZB#IC 3961 #PC#1#FR#1101046#FT#1539#TO#1101030#TT#1911#&train=3961&datetime=2025-09-15T16:46:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 77, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "1:17 u.", + "accessibilityValue": "1 uur en 17 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 5, + "distanceInMeters": 116337 + }, + { + "idx": "1", + "name": "IC 1164", + "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#441788#TA#0#DA#150925#1S#1100921#1T#1814#LS#1101078#LT#1941#PU#784#RT#1#CA#IC#ZE#1164#ZB#IC 1164 #PC#1#FR#1100921#FT#1814#TO#1101078#TT#1941#", + "origin": { + "name": "Eindhoven Centraal", + "lng": 5.48138904571533, + "lat": 51.4433326721191, + "countryCode": "NL", + "uicCode": "8400206", + "uicCdCode": "118400206", + "stationCode": "EHV", + "type": "STATION", + "plannedTimeZoneOffset": 120, + "plannedDateTime": "2025-09-15T18:14:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:14:00+0200", + "plannedTrack": "5", + "actualTrack": "5", + "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-15T19:15:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T19:15:00+0200", + "plannedTrack": "12", + "actualTrack": "12", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "1164", + "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": "2 tussenstops", + "shortValue": "2 tussenstops", + "accessibilityValue": "2 tussenstops", + "key": "PRODUCT_INTERMEDIATE_STOPS", + "noteType": "ATTRIBUTE", + "isPresentationRequired": true, + "nesProperties": { + "color": "text-body" + } + } + ] + ] + }, + "notes": [ + { + "value": "SHORTER_TRAIN", + "accessibilityValue": "SHORTER_TRAIN", + "key": "trainSize", + "noteType": "UNKNOWN", + "isPresentationRequired": false + } + ], + "messages": [ + { + "text": "Kortere trein, extra druk", + "type": "SHORTENED", + "nesProperties": { + "color": "text-warning-contrast", + "type": "warning", + "icon": "alert" + } + } + ], + "transferMessages": [ + { + "message": "11 min. overstaptijd", + "accessibilityMessage": "11 minuten overstaptijd", + "type": "TRANSFER_TIME", + "messageNesProperties": { + "color": "text-default", + "type": "informative" + } + } + ], + "stops": [ + { + "uicCode": "8400206", + "uicCdCode": "118400206", + "name": "Eindhoven Centraal", + "lat": 51.4433326721191, + "lng": 5.48138904571533, + "countryCode": "NL", + "notes": [], + "routeIdx": 0, + "plannedDepartureDateTime": "2025-09-15T18:14:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:14:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "5", + "plannedDepartureTrack": "5", + "plannedArrivalTrack": "5", + "actualArrivalTrack": "5", + "departureDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + }, + { + "uicCode": "8400597", + "uicCdCode": "118400597", + "name": "Tilburg", + "lat": 51.5605545043945, + "lng": 5.08361101150513, + "countryCode": "NL", + "notes": [], + "routeIdx": 5, + "plannedDepartureDateTime": "2025-09-15T18:38:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:38:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T18:35:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:35:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "2", + "plannedDepartureTrack": "2", + "plannedArrivalTrack": "2", + "actualArrivalTrack": "2", + "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": 9, + "plannedDepartureDateTime": "2025-09-15T18:53:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:53:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T18:51:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:51:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "7", + "plannedDepartureTrack": "7", + "plannedArrivalTrack": "7", + "actualArrivalTrack": "7", + "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": 15, + "plannedArrivalDateTime": "2025-09-15T19:15:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T19:15:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "12", + "plannedDepartureTrack": "12", + "plannedArrivalTrack": "12", + "actualArrivalTrack": "12", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "HIGH", + "punctuality": 83.3, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#441788#TA#0#DA#150925#1S#1100921#1T#1814#LS#1101078#LT#1941#PU#784#RT#1#CA#IC#ZE#1164#ZB#IC 1164 #PC#1#FR#1100921#FT#1814#TO#1101078#TT#1941#&train=1164&datetime=2025-09-15T18:14:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 61, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "1:01 u.", + "accessibilityValue": "1 uur en 1 minuut", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "distanceInMeters": 101727 + } + ], + "checksum": "b7d9a85e_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": "Rotterdam Centraal", + "lng": 4.46888875961304, + "lat": 51.9249992370605, + "countryCode": "NL", + "uicCode": "8400530", + "uicCdCode": "118400530", + "stationCode": "RTD", + "type": "STATION" + }, + "operator": "NS", + "productTypes": ["TRAIN"], + "fares": [ + { + "priceInCents": 1900, + "priceInCentsExcludingSupplement": 1900, + "supplementInCents": 0, + "buyableTicketSupplementPriceInCents": 0, + "product": "OVCHIPKAART_ENKELE_REIS", + "travelClass": "SECOND_CLASS", + "discountType": "NO_DISCOUNT" + } + ], + "travelDate": "2025-09-15" + } + ], + "productFare": { + "priceInCents": 1900, + "priceInCentsExcludingSupplement": 1900, + "buyableTicketPriceInCents": 1900, + "buyableTicketPriceInCentsExcludingSupplement": 1900, + "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/1646/1915?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%3A46%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A15%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D3511802831" + }, + "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%3A46%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A15%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D3511802831&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": "5", + "accessibilityName": "Intercity" + } + ] + }, + { + "idx": 5, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T16:54:00+02:00|plannedArrivalTime=2025-09-15T19:10:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1826949240", + "ctxRecon": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T16:54:00+02:00|plannedArrivalTime=2025-09-15T19:10:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1826949240", + "sourceCtxRecon": "¶HKI¶T$A=1@O=Amsterdam Centraal@L=1100836@a=128@$A=1@O=Utrecht Centraal@L=1100728@a=128@$202509151654$202509151721$IC 3061 $$1$$$$$$§W$A=1@O=Utrecht Centraal@L=1100728@a=128@$A=1@O=Utrecht Centraal@L=1100905@a=128@$202509151721$202509151723$$$1$$$$$$§T$A=1@O=Utrecht Centraal@L=1100905@a=128@$A=1@O='s-Hertogenbosch@L=1100870@a=128@$202509151724$202509151752$IC 3561 $$3$$$$$$§W$A=1@O='s-Hertogenbosch@L=1100870@a=128@$A=1@O='s-Hertogenbosch@L=1101032@a=128@$202509151752$202509151756$$$1$$$$$$§T$A=1@O='s-Hertogenbosch@L=1101032@a=128@$A=1@O=Utrecht Centraal@L=1100715@a=128@$202509151800$202509151827$IC 2762 $$1$$$$$$§W$A=1@O=Utrecht Centraal@L=1100715@a=128@$A=1@O=Utrecht Centraal@L=1100672@a=128@$202509151827$202509151832$$$1$$$$$$§T$A=1@O=Utrecht Centraal@L=1100672@a=128@$A=1@O=Rotterdam Centraal@L=1100690@a=128@$202509151833$202509151910$IC 2862 $$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#¶KCC¶#VE#0#ERG#45319#HIN#460#ECK#13985|13974|14107|14110|0|0|485|13955|6|0|8|0|0|-2147483648#¶KRCC¶#VE#1#MRTF#", + "plannedDurationInMinutes": 136, + "actualDurationInMinutes": 136, + "transfers": 3, + "status": "NORMAL", + "messages": [], + "legs": [ + { + "idx": "0", + "name": "IC 3061", + "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#444552#TA#0#DA#150925#1S#1101022#1T#1534#LS#1101149#LT#1817#PU#784#RT#1#CA#IC#ZE#3061#ZB#IC 3061 #PC#1#FR#1101022#FT#1534#TO#1101149#TT#1817#", + "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:54:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T16:54:00+0200", + "plannedTrack": "4", + "actualTrack": "4", + "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:21:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:21:00+0200", + "plannedTrack": "19", + "actualTrack": "19", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3061", + "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-15T16:54:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T16:54:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 0, + "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-15T17:02:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:02:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:01:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:01:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "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-15T17:21:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:21:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "19", + "plannedDepartureTrack": "19", + "plannedArrivalTrack": "19", + "actualArrivalTrack": "19", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 12, + "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#444552#TA#0#DA#150925#1S#1101022#1T#1534#LS#1101149#LT#1817#PU#784#RT#1#CA#IC#ZE#3061#ZB#IC 3061 #PC#1#FR#1101022#FT#1534#TO#1101149#TT#1817#&train=3061&datetime=2025-09-15T16:54: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": 2, + "distanceInMeters": 37529 + }, + { + "idx": "1", + "name": "IC 3561", + "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#507294#TA#0#DA#150925#1S#1101068#1T#1534#LS#1100958#LT#1858#PU#784#RT#3#CA#IC#ZE#3561#ZB#IC 3561 #PC#1#FR#1101068#FT#1534#TO#1100958#TT#1858#", + "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-15T17:24:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:24: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:52:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:52:00+0200", + "plannedTrack": "6", + "actualTrack": "6", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3561", + "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-15T17:24:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:24: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:52:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:52:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "6", + "plannedDepartureTrack": "6", + "plannedArrivalTrack": "6", + "actualArrivalTrack": "6", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "HIGH", + "bicycleSpotCount": 12, + "punctuality": 90.0, + "crossPlatformTransfer": false, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#507294#TA#0#DA#150925#1S#1101068#1T#1534#LS#1100958#LT#1858#PU#784#RT#3#CA#IC#ZE#3561#ZB#IC 3561 #PC#1#FR#1101068#FT#1534#TO#1100958#TT#1858#&train=3561&datetime=2025-09-15T17:24: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 2762", + "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#443724#TA#0#DA#150925#1S#1101011#1T#1629#LS#1101016#LT#1933#PU#784#RT#1#CA#IC#ZE#2762#ZB#IC 2762 #PC#1#FR#1101011#FT#1629#TO#1101016#TT#1933#", + "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-15T18:00:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:00: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-15T18:27:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:27:00+0200", + "plannedTrack": "7", + "actualTrack": "7", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "2762", + "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-15T18:00:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:00: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-15T18:27:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:27:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "7", + "plannedDepartureTrack": "7", + "plannedArrivalTrack": "7", + "actualArrivalTrack": "7", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 6, + "crossPlatformTransfer": false, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#443724#TA#0#DA#150925#1S#1101011#1T#1629#LS#1101016#LT#1933#PU#784#RT#1#CA#IC#ZE#2762#ZB#IC 2762 #PC#1#FR#1101011#FT#1629#TO#1101016#TT#1933#&train=2762&datetime=2025-09-15T18:00: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 2862", + "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#1137#TA#2#DA#150925#1S#1100672#1T#1833#LS#1100690#LT#1910#PU#784#RT#1#CA#IC#ZE#2862#ZB#IC 2862 #PC#1#FR#1100672#FT#1833#TO#1100690#TT#1910#", + "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-15T18:33:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:33:00+0200", + "plannedTrack": "11", + "actualTrack": "11", + "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-15T19:10:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T19:10:00+0200", + "plannedTrack": "14", + "actualTrack": "14", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "2862", + "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-15T18:33:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:33:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "11", + "plannedDepartureTrack": "11", + "plannedArrivalTrack": "11", + "actualArrivalTrack": "11", + "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-15T18:52:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:52:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T18:51:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:51: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-15T19:01:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T19:01:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T19:01:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T19:01: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-15T19:10:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T19:10:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "14", + "plannedDepartureTrack": "14", + "plannedArrivalTrack": "14", + "actualArrivalTrack": "14", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 6, + "punctuality": 88.9, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1137#TA#2#DA#150925#1S#1100672#1T#1833#LS#1100690#LT#1910#PU#784#RT#1#CA#IC#ZE#2862#ZB#IC 2862 #PC#1#FR#1100672#FT#1833#TO#1100690#TT#1910#&train=2862&datetime=2025-09-15T18:33: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": "bf4e4bd4_3", + "crowdForecast": "HIGH", + "punctuality": 88.9, + "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/1654/1910?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%3A54%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A10%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D1826949240" + }, + "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%3A54%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A10%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D1826949240&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": "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": "11", + "accessibilityName": "Intercity" + } + ] + }, + { + "idx": 6, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T17:05:00+02:00|plannedArrivalTime=2025-09-15T19:07:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1121055257", + "ctxRecon": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T17:05:00+02:00|plannedArrivalTime=2025-09-15T19:07:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=1121055257", + "sourceCtxRecon": "¶HKI¶T$A=1@O=Amsterdam Centraal@L=1100836@a=128@$A=1@O='s-Hertogenbosch@L=1100870@a=128@$202509151705$202509151802$IC 2763 $$1$$$$$$§W$A=1@O='s-Hertogenbosch@L=1100870@a=128@$A=1@O='s-Hertogenbosch@L=1101751@a=128@$202509151802$202509151804$$$1$$$$$$§T$A=1@O='s-Hertogenbosch@L=1101751@a=128@$A=1@O=Breda@L=1101034@a=128@$202509151811$202509151840$IC 3663 $$3$$$$$$§W$A=1@O=Breda@L=1101034@a=128@$A=1@O=Breda@L=1100942@a=128@$202509151840$202509151842$$$1$$$$$$§T$A=1@O=Breda@L=1100942@a=128@$A=1@O=Rotterdam Centraal@L=1100726@a=128@$202509151844$202509151907$ICD 1873$$1$$$$$$¶KC¶#VE#2#CF#100#CA#0#CM#0#SICT#0#AM#16465#AM2#0#RT#31#¶KCC¶#VE#0#ERG#261#HIN#390#ECK#13985|13985|14107|14107|0|0|66021|13955|7|0|2|0|0|-2147483648#¶KRCC¶#VE#1#MRTF#", + "plannedDurationInMinutes": 122, + "actualDurationInMinutes": 122, + "transfers": 2, + "status": "NORMAL", + "messages": [], + "legs": [ + { + "idx": "0", + "name": "IC 2763", + "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#1090#TA#0#DA#150925#1S#1101009#1T#1627#LS#1101011#LT#1933#PU#784#RT#1#CA#IC#ZE#2763#ZB#IC 2763 #PC#1#FR#1101009#FT#1627#TO#1101011#TT#1933#", + "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-15T17:05:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:05: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-15T18:02:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:02:00+0200", + "plannedTrack": "6", + "actualTrack": "6", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "2763", + "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-15T17:05:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:05:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 0, + "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-15T17:13:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:13:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:13:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:13:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "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:34:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:34:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:31:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:31:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "18", + "plannedDepartureTrack": "18", + "plannedArrivalTrack": "18", + "actualArrivalTrack": "18", + "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-15T18:02:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:02:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "6", + "plannedDepartureTrack": "6", + "plannedArrivalTrack": "6", + "actualArrivalTrack": "6", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 12, + "crossPlatformTransfer": true, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1090#TA#0#DA#150925#1S#1101009#1T#1627#LS#1101011#LT#1933#PU#784#RT#1#CA#IC#ZE#2763#ZB#IC 2763 #PC#1#FR#1101009#FT#1627#TO#1101011#TT#1933#&train=2763&datetime=2025-09-15T17:05:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 57, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "57 min.", + "accessibilityValue": "57 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 2, + "distanceInMeters": 84795 + }, + { + "idx": "1", + "name": "IC 3663", + "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#505598#TA#0#DA#150925#1S#1101167#1T#1620#LS#1101102#LT#1903#PU#784#RT#3#CA#IC#ZE#3663#ZB#IC 3663 #PC#1#FR#1101167#FT#1620#TO#1101102#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-15T18:11:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:11: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:40:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:40:00+0200", + "plannedTrack": "8", + "actualTrack": "8", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3663", + "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-15T18:11:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:11: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-15T18:28:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:28:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T18:26:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:26: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:40:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:40:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "8", + "plannedDepartureTrack": "8", + "plannedArrivalTrack": "8", + "actualArrivalTrack": "8", + "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#505598#TA#0#DA#150925#1S#1101167#1T#1620#LS#1101102#LT#1903#PU#784#RT#3#CA#IC#ZE#3663#ZB#IC 3663 #PC#1#FR#1101167#FT#1620#TO#1101102#TT#1903#&train=3663&datetime=2025-09-15T18:11: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 1873", + "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#442518#TA#0#DA#150925#1S#1100942#1T#1844#LS#1100687#LT#2024#PU#784#RT#1#CA#ICD#ZE#1873#ZB#ICD 1873#PC#1#FR#1100942#FT#1844#TO#1100687#TT#2024#", + "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:44:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:44: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-15T19:07:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T19:07:00+0200", + "plannedTrack": "12", + "actualTrack": "12", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "1873", + "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-15T18:44:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:44: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-15T19:07:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T19:07: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#442518#TA#0#DA#150925#1S#1100942#1T#1844#LS#1100687#LT#2024#PU#784#RT#1#CA#ICD#ZE#1873#ZB#ICD 1873#PC#1#FR#1100942#FT#1844#TO#1100687#TT#2024#&train=1873&datetime=2025-09-15T18:44: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": "78085762_3", + "crowdForecast": "MEDIUM", + "punctuality": 100.0, + "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/1705/1907?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-15T17%3A05%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A07%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D1121055257" + }, + "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-15T17%3A05%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A07%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D1121055257&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": 7, + "uid": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T17:05:00+02:00|plannedArrivalTime=2025-09-15T19:15:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=459261293", + "ctxRecon": "arnu|fromStation=8400058|requestedFromStation=8400058|toStation=8400530|requestedToStation=8400530|viaStation=8400319|plannedFromTime=2025-09-15T17:05:00+02:00|plannedArrivalTime=2025-09-15T19:15:00+02:00|excludeHighSpeedTrains=false|searchForAccessibleTrip=false|localTrainsOnly=false|disabledTransportModalities=BUS,FERRY,TRAM,METRO|travelAssistance=false|tripSummaryHash=459261293", + "sourceCtxRecon": "¶HKI¶T$A=1@O=Amsterdam Centraal@L=1100836@a=128@$A=1@O='s-Hertogenbosch@L=1100870@a=128@$202509151705$202509151802$IC 2763 $$1$$$$$$§W$A=1@O='s-Hertogenbosch@L=1100870@a=128@$A=1@O='s-Hertogenbosch@L=1101751@a=128@$202509151802$202509151804$$$1$$$$$$§T$A=1@O='s-Hertogenbosch@L=1101751@a=128@$A=1@O=Breda@L=1101034@a=128@$202509151811$202509151840$IC 3663 $$3$$$$$$§W$A=1@O=Breda@L=1101034@a=128@$A=1@O=Breda@L=1100942@a=128@$202509151840$202509151842$$$1$$$$$$§T$A=1@O=Breda@L=1100942@a=128@$A=1@O=Rotterdam Centraal@L=1100726@a=128@$202509151853$202509151915$IC 1164 $$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#13985|13985|14107|14115|0|0|485|13955|8|0|8|0|0|-2147483648#¶KRCC¶#VE#1#MRTF#", + "plannedDurationInMinutes": 130, + "actualDurationInMinutes": 130, + "transfers": 2, + "status": "NORMAL", + "messages": [], + "legs": [ + { + "idx": "0", + "name": "IC 2763", + "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#1090#TA#0#DA#150925#1S#1101009#1T#1627#LS#1101011#LT#1933#PU#784#RT#1#CA#IC#ZE#2763#ZB#IC 2763 #PC#1#FR#1101009#FT#1627#TO#1101011#TT#1933#", + "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-15T17:05:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T17:05: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-15T18:02:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:02:00+0200", + "plannedTrack": "6", + "actualTrack": "6", + "exitSide": "RIGHT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "2763", + "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-15T17:05:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:05:00+0200", + "actualDepartureTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 0, + "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-15T17:13:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:13:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:13:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:13:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "4", + "plannedDepartureTrack": "4", + "plannedArrivalTrack": "4", + "actualArrivalTrack": "4", + "departureDelayInSeconds": 0, + "arrivalDelayInSeconds": 0, + "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:34:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T17:34:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T17:31:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T17:31:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "18", + "plannedDepartureTrack": "18", + "plannedArrivalTrack": "18", + "actualArrivalTrack": "18", + "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-15T18:02:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:02:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "6", + "plannedDepartureTrack": "6", + "plannedArrivalTrack": "6", + "actualArrivalTrack": "6", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "LOW", + "bicycleSpotCount": 12, + "crossPlatformTransfer": true, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#1090#TA#0#DA#150925#1S#1101009#1T#1627#LS#1101011#LT#1933#PU#784#RT#1#CA#IC#ZE#2763#ZB#IC 2763 #PC#1#FR#1101009#FT#1627#TO#1101011#TT#1933#&train=2763&datetime=2025-09-15T17:05:00+02:00" + } + } + ], + "reachable": true, + "plannedDurationInMinutes": 57, + "nesProperties": { + "color": "text-info", + "scope": "LEG_LINE", + "styles": { + "type": "LineStyles", + "dashed": false + } + }, + "duration": { + "value": "57 min.", + "accessibilityValue": "57 minuten", + "nesProperties": { + "color": "text-body" + } + }, + "preSteps": [], + "postSteps": [], + "transferTimeToNextLeg": 2, + "distanceInMeters": 84795 + }, + { + "idx": "1", + "name": "IC 3663", + "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#505598#TA#0#DA#150925#1S#1101167#1T#1620#LS#1101102#LT#1903#PU#784#RT#3#CA#IC#ZE#3663#ZB#IC 3663 #PC#1#FR#1101167#FT#1620#TO#1101102#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-15T18:11:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:11: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:40:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:40:00+0200", + "plannedTrack": "8", + "actualTrack": "8", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "3663", + "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-15T18:11:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:11: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-15T18:28:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:28:00+0200", + "actualDepartureTimeZoneOffset": 120, + "plannedArrivalDateTime": "2025-09-15T18:26:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:26: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:40:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T18:40:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "8", + "plannedDepartureTrack": "8", + "plannedArrivalTrack": "8", + "actualArrivalTrack": "8", + "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#505598#TA#0#DA#150925#1S#1101167#1T#1620#LS#1101102#LT#1903#PU#784#RT#3#CA#IC#ZE#3663#ZB#IC 3663 #PC#1#FR#1101167#FT#1620#TO#1101102#TT#1903#&train=3663&datetime=2025-09-15T18:11: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 1164", + "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#441788#TA#0#DA#150925#1S#1100921#1T#1814#LS#1101078#LT#1941#PU#784#RT#1#CA#IC#ZE#1164#ZB#IC 1164 #PC#1#FR#1100921#FT#1814#TO#1101078#TT#1941#", + "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:53:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T18:53: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-15T19:15:00+0200", + "actualTimeZoneOffset": 120, + "actualDateTime": "2025-09-15T19:15:00+0200", + "plannedTrack": "12", + "actualTrack": "12", + "exitSide": "LEFT", + "checkinStatus": "NOTHING", + "notes": [] + }, + "product": { + "productType": "Product", + "number": "1164", + "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:53:00+0200", + "plannedDepartureTimeZoneOffset": 120, + "actualDepartureDateTime": "2025-09-15T18:53: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-15T19:15:00+0200", + "plannedArrivalTimeZoneOffset": 120, + "actualArrivalDateTime": "2025-09-15T19:15:00+0200", + "actualArrivalTimeZoneOffset": 120, + "actualDepartureTrack": "12", + "plannedDepartureTrack": "12", + "plannedArrivalTrack": "12", + "actualArrivalTrack": "12", + "arrivalDelayInSeconds": 0, + "cancelled": false, + "borderStop": false, + "passing": false + } + ], + "crowdForecast": "MEDIUM", + "punctuality": 83.3, + "shorterStock": false, + "journeyDetail": [ + { + "type": "TRAIN_XML", + "link": { + "uri": "/api/v2/journey?id=HARP_MM-2|#VN#1#ST#1757498654#PI#0#ZI#441788#TA#0#DA#150925#1S#1100921#1T#1814#LS#1101078#LT#1941#PU#784#RT#1#CA#IC#ZE#1164#ZB#IC 1164 #PC#1#FR#1100921#FT#1814#TO#1101078#TT#1941#&train=1164&datetime=2025-09-15T18:53: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": "16db0b8e_3", + "crowdForecast": "MEDIUM", + "punctuality": 83.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/1705/1915?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-15T17%3A05%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A15%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D459261293" + }, + "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-15T17%3A05%3A00%2B02%3A00%7CplannedArrivalTime%3D2025-09-15T19%3A15%3A00%2B02%3A00%7CexcludeHighSpeedTrains%3Dfalse%7CsearchForAccessibleTrip%3Dfalse%7ClocalTrainsOnly%3Dfalse%7CdisabledTransportModalities%3DBUS%2CFERRY%2CTRAM%2CMETRO%7CtravelAssistance%3Dfalse%7CtripSummaryHash%3D459261293&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 new file mode 100644 index 00000000000000..27e2cbb0147010 --- /dev/null +++ b/tests/components/nederlandse_spoorwegen/snapshots/test_sensor.ambr @@ -0,0 +1,37 @@ +# serializer version: 1 +# name: test_sensor + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'arrival_delay': False, + 'arrival_platform_actual': '12', + 'arrival_platform_planned': '12', + 'arrival_time_actual': '18:37', + 'arrival_time_planned': '18:37', + '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', + 'friendly_name': 'To work', + 'going': True, + 'icon': 'mdi:train', + 'next': '16:46', + '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': '16:35', + }) +# --- diff --git a/tests/components/nederlandse_spoorwegen/test_sensor.py b/tests/components/nederlandse_spoorwegen/test_sensor.py index c748e126948d8e..8c90b0f96ce835 100644 --- a/tests/components/nederlandse_spoorwegen/test_sensor.py +++ b/tests/components/nederlandse_spoorwegen/test_sensor.py @@ -2,6 +2,9 @@ from unittest.mock import AsyncMock +import pytest +from syrupy.assertion import SnapshotAssertion + from homeassistant.components.nederlandse_spoorwegen.const import ( CONF_FROM, CONF_ROUTES, @@ -15,8 +18,11 @@ import homeassistant.helpers.issue_registry as ir from homeassistant.setup import async_setup_component +from . import setup_integration from .const import API_KEY +from tests.common import MockConfigEntry + async def test_config_import( hass: HomeAssistant, @@ -51,3 +57,16 @@ async def test_config_import( assert len(issue_registry.issues) == 1 assert (HOMEASSISTANT_DOMAIN, "deprecated_yaml") in issue_registry.issues assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +@pytest.mark.freeze_time("2025-09-15 14:30:00+00:00") +async def test_sensor( + hass: HomeAssistant, + mock_nsapi, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test sensor initialization.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.to_work") == snapshot diff --git a/tests/components/radio_browser/conftest.py b/tests/components/radio_browser/conftest.py index fc666b32c53ddb..24bd93e48a7557 100644 --- a/tests/components/radio_browser/conftest.py +++ b/tests/components/radio_browser/conftest.py @@ -3,11 +3,12 @@ from __future__ import annotations from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.radio_browser.const import DOMAIN +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -29,3 +30,108 @@ def mock_setup_entry() -> Generator[AsyncMock]: "homeassistant.components.radio_browser.async_setup_entry", return_value=True ) as mock_setup: yield mock_setup + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> MockConfigEntry: + """Set up the Radio Browser integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry + + +@pytest.fixture +def mock_countries(): + "Generate mock countries for the countries method of the radios object." + + class MockCountry: + """Country Object for Radios.""" + + def __init__(self, code, name) -> None: + """Initialize a mock country.""" + self.code = code + self.name = name + self.favicon = "fake.png" + + return [MockCountry("US", "United States")] + + +@pytest.fixture +def mock_stations(): + "Generate mock stations for the stations method of the radios object." + + class MockStation: + """Station object for Radios.""" + + def __init__(self, country_code, latitude, longitude, name, uuid) -> None: + """Initialize a mock station.""" + self.country_code = country_code + self.latitude = latitude + self.longitude = longitude + self.uuid = uuid + self.name = name + self.codec = "MP3" + self.favicon = "fake.png" + + return [ + MockStation( + country_code="US", + latitude=45.52000, + longitude=-122.63961, + name="Near Station 1", + uuid="1", + ), + MockStation( + country_code="US", + latitude=None, + longitude=None, + name="Unknown location station", + uuid="2", + ), + MockStation( + country_code="US", + latitude=47.57071, + longitude=-122.21148, + name="Moderate Far Station", + uuid="3", + ), + MockStation( + country_code="US", + latitude=45.73943, + longitude=-121.51859, + name="Near Station 2", + uuid="4", + ), + MockStation( + country_code="US", + latitude=44.99026, + longitude=-69.27804, + name="Really Far Station", + uuid="5", + ), + ] + + +@pytest.fixture +def mock_radios(mock_countries, mock_stations): + """Provide a radios mock object.""" + radios = MagicMock() + radios.countries = AsyncMock(return_value=mock_countries) + radios.stations = AsyncMock(return_value=mock_stations) + return radios + + +@pytest.fixture +def patch_radios(monkeypatch: pytest.MonkeyPatch, mock_radios): + """Replace the radios object in the source with the mock object (with mock stations and countries).""" + + def _patch(source): + monkeypatch.setattr(type(source), "radios", mock_radios) + + return _patch diff --git a/tests/components/radio_browser/test_media_source.py b/tests/components/radio_browser/test_media_source.py new file mode 100644 index 00000000000000..a9d08c1e438d41 --- /dev/null +++ b/tests/components/radio_browser/test_media_source.py @@ -0,0 +1,73 @@ +"""Tests for radio_browser media_source.""" + +from unittest.mock import AsyncMock + +import pytest +from radios import FilterBy, Order + +from homeassistant.components import media_source +from homeassistant.components.radio_browser.media_source import async_get_media_source +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +DOMAIN = "radio_browser" + + +@pytest.fixture(autouse=True) +async def setup_media_source(hass: HomeAssistant) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + +async def test_browsing_local( + hass: HomeAssistant, init_integration: AsyncMock, patch_radios +) -> None: + """Test browsing local stations.""" + + hass.config.latitude = 45.58539 + hass.config.longitude = -122.40320 + hass.config.country = "US" + + source = await async_get_media_source(hass) + patch_radios(source) + + item = await media_source.async_browse_media( + hass, f"{media_source.URI_SCHEME}{DOMAIN}" + ) + + assert item is not None + assert item.title == "My Radios" + assert item.children is not None + assert len(item.children) == 5 + assert item.can_play is False + assert item.can_expand is True + + assert item.children[3].title == "Local stations" + + item_child = await media_source.async_browse_media( + hass, item.children[3].media_content_id + ) + + source.radios.stations.assert_awaited_with( + filter_by=FilterBy.COUNTRY_CODE_EXACT, + filter_term=hass.config.country, + hide_broken=True, + order=Order.NAME, + reverse=False, + ) + + assert item_child is not None + assert item_child.title == "My Radios" + assert len(item_child.children) == 2 + assert item_child.children[0].title == "Near Station 1" + assert item_child.children[1].title == "Near Station 2" + + # Test browsing a different category to hit the path where async_build_local + # returns [] + other_browse = await media_source.async_browse_media( + hass, f"{media_source.URI_SCHEME}{DOMAIN}/nonexistent" + ) + + assert other_browse is not None + assert other_browse.title == "My Radios" + assert len(other_browse.children) == 0