mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add uk_transport component. (#8600)
This commit is contained in:
parent
3318f02664
commit
3b4ea864a1
275
homeassistant/components/sensor/uk_transport.py
Normal file
275
homeassistant/components/sensor/uk_transport.py
Normal file
@ -0,0 +1,275 @@
|
||||
"""Support for UK public transport data provided by transportapi.com.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.uk_transport/
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ATCOCODE = 'atcocode'
|
||||
ATTR_LOCALITY = 'locality'
|
||||
ATTR_STOP_NAME = 'stop_name'
|
||||
ATTR_REQUEST_TIME = 'request_time'
|
||||
ATTR_NEXT_BUSES = 'next_buses'
|
||||
ATTR_STATION_CODE = 'station_code'
|
||||
ATTR_CALLING_AT = 'calling_at'
|
||||
ATTR_NEXT_TRAINS = 'next_trains'
|
||||
|
||||
CONF_API_APP_KEY = 'app_key'
|
||||
CONF_API_APP_ID = 'app_id'
|
||||
CONF_QUERIES = 'queries'
|
||||
CONF_MODE = 'mode'
|
||||
CONF_ORIGIN = 'origin'
|
||||
CONF_DESTINATION = 'destination'
|
||||
|
||||
_QUERY_SCHEME = vol.Schema({
|
||||
vol.Required(CONF_MODE):
|
||||
vol.All(cv.ensure_list, [vol.In(list(['bus', 'train']))]),
|
||||
vol.Required(CONF_ORIGIN): cv.string,
|
||||
vol.Required(CONF_DESTINATION): cv.string,
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_APP_ID): cv.string,
|
||||
vol.Required(CONF_API_APP_KEY): cv.string,
|
||||
vol.Required(CONF_QUERIES): [_QUERY_SCHEME],
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Get the uk_transport sensor."""
|
||||
sensors = []
|
||||
number_sensors = len(config.get(CONF_QUERIES))
|
||||
interval = timedelta(seconds=87*number_sensors)
|
||||
|
||||
for query in config.get(CONF_QUERIES):
|
||||
if 'bus' in query.get(CONF_MODE):
|
||||
stop_atcocode = query.get(CONF_ORIGIN)
|
||||
bus_direction = query.get(CONF_DESTINATION)
|
||||
sensors.append(
|
||||
UkTransportLiveBusTimeSensor(
|
||||
config.get(CONF_API_APP_ID),
|
||||
config.get(CONF_API_APP_KEY),
|
||||
stop_atcocode,
|
||||
bus_direction,
|
||||
interval))
|
||||
|
||||
elif 'train' in query.get(CONF_MODE):
|
||||
station_code = query.get(CONF_ORIGIN)
|
||||
calling_at = query.get(CONF_DESTINATION)
|
||||
sensors.append(
|
||||
UkTransportLiveTrainTimeSensor(
|
||||
config.get(CONF_API_APP_ID),
|
||||
config.get(CONF_API_APP_KEY),
|
||||
station_code,
|
||||
calling_at,
|
||||
interval))
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class UkTransportSensor(Entity):
|
||||
"""
|
||||
Sensor that reads the UK transport web API.
|
||||
|
||||
transportapi.com provides comprehensive transport data for UK train, tube
|
||||
and bus travel across the UK via simple JSON API. Subclasses of this
|
||||
base class can be used to access specific types of information.
|
||||
"""
|
||||
|
||||
TRANSPORT_API_URL_BASE = "https://transportapi.com/v3/uk/"
|
||||
ICON = 'mdi:train'
|
||||
|
||||
def __init__(self, name, api_app_id, api_app_key, url):
|
||||
"""Initialize the sensor."""
|
||||
self._data = {}
|
||||
self._api_app_id = api_app_id
|
||||
self._api_app_key = api_app_key
|
||||
self._url = self.TRANSPORT_API_URL_BASE + url
|
||||
self._name = name
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit this state is expressed in."""
|
||||
return "min"
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self.ICON
|
||||
|
||||
def _do_api_request(self, params):
|
||||
"""Perform an API request."""
|
||||
request_params = dict({
|
||||
'app_id': self._api_app_id,
|
||||
'app_key': self._api_app_key,
|
||||
}, **params)
|
||||
|
||||
response = requests.get(self._url, params=request_params)
|
||||
if response.status_code != 200:
|
||||
_LOGGER.warning('Invalid response from API')
|
||||
elif 'error' in response.json():
|
||||
if 'exceeded' in response.json()['error']:
|
||||
self._state = 'Useage limites exceeded'
|
||||
if 'invalid' in response.json()['error']:
|
||||
self._state = 'Credentials invalid'
|
||||
else:
|
||||
self._data = response.json()
|
||||
|
||||
|
||||
class UkTransportLiveBusTimeSensor(UkTransportSensor):
|
||||
"""Live bus time sensor from UK transportapi.com."""
|
||||
|
||||
ICON = 'mdi:bus'
|
||||
|
||||
def __init__(self, api_app_id, api_app_key,
|
||||
stop_atcocode, bus_direction, interval):
|
||||
"""Construct a live bus time sensor."""
|
||||
self._stop_atcocode = stop_atcocode
|
||||
self._bus_direction = bus_direction
|
||||
self._next_buses = []
|
||||
self._destination_re = re.compile(
|
||||
'{}'.format(bus_direction), re.IGNORECASE
|
||||
)
|
||||
|
||||
sensor_name = 'Next bus to {}'.format(bus_direction)
|
||||
stop_url = 'bus/stop/{}/live.json'.format(stop_atcocode)
|
||||
|
||||
UkTransportSensor.__init__(
|
||||
self, sensor_name, api_app_id, api_app_key, stop_url
|
||||
)
|
||||
self.update = Throttle(interval)(self._update)
|
||||
|
||||
def _update(self):
|
||||
"""Get the latest live departure data for the specified stop."""
|
||||
params = {'group': 'route', 'nextbuses': 'no'}
|
||||
|
||||
self._do_api_request(params)
|
||||
|
||||
if self._data != {}:
|
||||
self._next_buses = []
|
||||
|
||||
for (route, departures) in self._data['departures'].items():
|
||||
for departure in departures:
|
||||
if self._destination_re.search(departure['direction']):
|
||||
self._next_buses.append({
|
||||
'route': route,
|
||||
'direction': departure['direction'],
|
||||
'scheduled': departure['aimed_departure_time'],
|
||||
'estimated': departure['best_departure_estimate']
|
||||
})
|
||||
|
||||
self._state = min(map(
|
||||
_delta_mins, [bus['scheduled'] for bus in self._next_buses]
|
||||
))
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return other details about the sensor state."""
|
||||
attrs = {}
|
||||
if self._data is not None:
|
||||
for key in [
|
||||
ATTR_ATCOCODE, ATTR_LOCALITY, ATTR_STOP_NAME,
|
||||
ATTR_REQUEST_TIME
|
||||
]:
|
||||
attrs[key] = self._data.get(key)
|
||||
attrs[ATTR_NEXT_BUSES] = self._next_buses
|
||||
return attrs
|
||||
|
||||
|
||||
class UkTransportLiveTrainTimeSensor(UkTransportSensor):
|
||||
"""Live train time sensor from UK transportapi.com."""
|
||||
|
||||
ICON = 'mdi:train'
|
||||
|
||||
def __init__(self, api_app_id, api_app_key,
|
||||
station_code, calling_at, interval):
|
||||
"""Construct a live bus time sensor."""
|
||||
self._station_code = station_code
|
||||
self._calling_at = calling_at
|
||||
self._next_trains = []
|
||||
|
||||
sensor_name = 'Next train to {}'.format(calling_at)
|
||||
query_url = 'train/station/{}/live.json'.format(station_code)
|
||||
|
||||
UkTransportSensor.__init__(
|
||||
self, sensor_name, api_app_id, api_app_key, query_url
|
||||
)
|
||||
self.update = Throttle(interval)(self._update)
|
||||
|
||||
def _update(self):
|
||||
"""Get the latest live departure data for the specified stop."""
|
||||
params = {'darwin': 'false',
|
||||
'calling_at': self._calling_at,
|
||||
'train_status': 'passenger'}
|
||||
|
||||
self._do_api_request(params)
|
||||
self._next_trains = []
|
||||
|
||||
if self._data != {}:
|
||||
if self._data['departures']['all'] == []:
|
||||
self._state = 'No departures'
|
||||
else:
|
||||
for departure in self._data['departures']['all']:
|
||||
self._next_trains.append({
|
||||
'origin_name': departure['origin_name'],
|
||||
'destination_name': departure['destination_name'],
|
||||
'status': departure['status'],
|
||||
'scheduled': departure['aimed_departure_time'],
|
||||
'estimated': departure['expected_departure_time'],
|
||||
'platform': departure['platform'],
|
||||
'operator_name': departure['operator_name']
|
||||
})
|
||||
|
||||
self._state = min(map(
|
||||
_delta_mins,
|
||||
[train['scheduled'] for train in self._next_trains]
|
||||
))
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return other details about the sensor state."""
|
||||
attrs = {}
|
||||
if self._data is not None:
|
||||
attrs[ATTR_STATION_CODE] = self._station_code
|
||||
attrs[ATTR_CALLING_AT] = self._calling_at
|
||||
if self._next_trains:
|
||||
attrs[ATTR_NEXT_TRAINS] = self._next_trains
|
||||
return attrs
|
||||
|
||||
|
||||
def _delta_mins(hhmm_time_str):
|
||||
"""Calculate time delta in minutes to a time in hh:mm format."""
|
||||
now = datetime.now()
|
||||
hhmm_time = datetime.strptime(hhmm_time_str, '%H:%M')
|
||||
|
||||
hhmm_datetime = datetime(
|
||||
now.year, now.month, now.day,
|
||||
hour=hhmm_time.hour, minute=hhmm_time.minute
|
||||
)
|
||||
if hhmm_datetime < now:
|
||||
hhmm_datetime += timedelta(days=1)
|
||||
|
||||
delta_mins = (hhmm_datetime - now).seconds // 60
|
||||
return delta_mins
|
93
tests/components/sensor/test_uk_transport.py
Normal file
93
tests/components/sensor/test_uk_transport.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""The tests for the uk_transport platform."""
|
||||
import re
|
||||
|
||||
import requests_mock
|
||||
import unittest
|
||||
|
||||
from homeassistant.components.sensor.uk_transport import (
|
||||
UkTransportSensor,
|
||||
ATTR_ATCOCODE, ATTR_LOCALITY, ATTR_STOP_NAME, ATTR_NEXT_BUSES,
|
||||
ATTR_STATION_CODE, ATTR_CALLING_AT, ATTR_NEXT_TRAINS,
|
||||
CONF_API_APP_KEY, CONF_API_APP_ID)
|
||||
from homeassistant.setup import setup_component
|
||||
from tests.common import load_fixture, get_test_home_assistant
|
||||
|
||||
BUS_ATCOCODE = '340000368SHE'
|
||||
BUS_DIRECTION = 'Wantage'
|
||||
TRAIN_STATION_CODE = 'WIM'
|
||||
TRAIN_DESTINATION_NAME = 'WAT'
|
||||
|
||||
VALID_CONFIG = {
|
||||
'platform': 'uk_transport',
|
||||
CONF_API_APP_ID: 'foo',
|
||||
CONF_API_APP_KEY: 'ebcd1234',
|
||||
'queries': [{
|
||||
'mode': 'bus',
|
||||
'origin': BUS_ATCOCODE,
|
||||
'destination': BUS_DIRECTION},
|
||||
{
|
||||
'mode': 'train',
|
||||
'origin': TRAIN_STATION_CODE,
|
||||
'destination': TRAIN_DESTINATION_NAME}]
|
||||
}
|
||||
|
||||
|
||||
class TestUkTransportSensor(unittest.TestCase):
|
||||
"""Test the uk_transport platform."""
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize values for this testcase class."""
|
||||
self.hass = get_test_home_assistant()
|
||||
self.config = VALID_CONFIG
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop everything that was started."""
|
||||
self.hass.stop()
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_bus(self, mock_req):
|
||||
"""Test for operational uk_transport sensor with proper attributes."""
|
||||
with requests_mock.Mocker() as mock_req:
|
||||
uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + '*')
|
||||
mock_req.get(uri, text=load_fixture('uk_transport_bus.json'))
|
||||
self.assertTrue(
|
||||
setup_component(self.hass, 'sensor', {'sensor': self.config}))
|
||||
|
||||
bus_state = self.hass.states.get('sensor.next_bus_to_wantage')
|
||||
|
||||
assert type(bus_state.state) == str
|
||||
assert bus_state.name == 'Next bus to {}'.format(BUS_DIRECTION)
|
||||
assert bus_state.attributes.get(ATTR_ATCOCODE) == BUS_ATCOCODE
|
||||
assert bus_state.attributes.get(ATTR_LOCALITY) == 'Harwell Campus'
|
||||
assert bus_state.attributes.get(ATTR_STOP_NAME) == 'Bus Station'
|
||||
assert len(bus_state.attributes.get(ATTR_NEXT_BUSES)) == 2
|
||||
|
||||
direction_re = re.compile(BUS_DIRECTION)
|
||||
for bus in bus_state.attributes.get(ATTR_NEXT_BUSES):
|
||||
print(bus['direction'], direction_re.match(bus['direction']))
|
||||
assert direction_re.search(bus['direction']) is not None
|
||||
|
||||
@requests_mock.Mocker()
|
||||
def test_train(self, mock_req):
|
||||
"""Test for operational uk_transport sensor with proper attributes."""
|
||||
with requests_mock.Mocker() as mock_req:
|
||||
uri = re.compile(UkTransportSensor.TRANSPORT_API_URL_BASE + '*')
|
||||
mock_req.get(uri, text=load_fixture('uk_transport_train.json'))
|
||||
self.assertTrue(
|
||||
setup_component(self.hass, 'sensor', {'sensor': self.config}))
|
||||
|
||||
train_state = self.hass.states.get('sensor.next_train_to_WAT')
|
||||
|
||||
assert type(train_state.state) == str
|
||||
assert train_state.name == 'Next train to {}'.format(
|
||||
TRAIN_DESTINATION_NAME)
|
||||
assert train_state.attributes.get(
|
||||
ATTR_STATION_CODE) == TRAIN_STATION_CODE
|
||||
assert train_state.attributes.get(
|
||||
ATTR_CALLING_AT) == TRAIN_DESTINATION_NAME
|
||||
assert len(train_state.attributes.get(ATTR_NEXT_TRAINS)) == 25
|
||||
|
||||
assert train_state.attributes.get(
|
||||
ATTR_NEXT_TRAINS)[0]['destination_name'] == 'London Waterloo'
|
||||
assert train_state.attributes.get(
|
||||
ATTR_NEXT_TRAINS)[0]['estimated'] == '06:13'
|
110
tests/fixtures/uk_transport_bus.json
vendored
Normal file
110
tests/fixtures/uk_transport_bus.json
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
{
|
||||
"atcocode": "340000368SHE",
|
||||
"bearing": "",
|
||||
"departures": {
|
||||
"32A": [{
|
||||
"aimed_departure_time": "10:18",
|
||||
"best_departure_estimate": "10:18",
|
||||
"date": "2017-05-09",
|
||||
"dir": "outbound",
|
||||
"direction": "Market Place (Wantage)",
|
||||
"expected_departure_date": null,
|
||||
"expected_departure_time": null,
|
||||
"id": "https://transportapi.com/v3/uk/bus/route/THTR/32A/outbound/340000368SHE/2017-05-09/10:18/timetable.json?app_id=e99&app_key=058",
|
||||
"line": "32A",
|
||||
"line_name": "32A",
|
||||
"mode": "bus",
|
||||
"operator": "THTR",
|
||||
"operator_name": "Thames Travel",
|
||||
"source": "Traveline timetable (not a nextbuses live region)"
|
||||
},
|
||||
{
|
||||
"aimed_departure_time": "11:00",
|
||||
"best_departure_estimate": "11:00",
|
||||
"date": "2017-05-09",
|
||||
"dir": "outbound",
|
||||
"direction": "Stratton Way (Abingdon Town Centre)",
|
||||
"expected_departure_date": null,
|
||||
"expected_departure_time": null,
|
||||
"id": "https://transportapi.com/v3/uk/bus/route/THTR/32A/outbound/340000368SHE/2017-05-09/11:00/timetable.json?app_id=e99&app_key=058",
|
||||
"line": "32A",
|
||||
"line_name": "32A",
|
||||
"mode": "bus",
|
||||
"operator": "THTR",
|
||||
"operator_name": "Thames Travel",
|
||||
"source": "Traveline timetable (not a nextbuses live region)"
|
||||
},
|
||||
{
|
||||
"aimed_departure_time": "11:18",
|
||||
"best_departure_estimate": "11:18",
|
||||
"date": "2017-05-09",
|
||||
"dir": "outbound",
|
||||
"direction": "Market Place (Wantage)",
|
||||
"expected_departure_date": null,
|
||||
"expected_departure_time": null,
|
||||
"id": "https://transportapi.com/v3/uk/bus/route/THTR/32A/outbound/340000368SHE/2017-05-09/11:18/timetable.json?app_id=e99&app_key=058",
|
||||
"line": "32A",
|
||||
"line_name": "32A",
|
||||
"mode": "bus",
|
||||
"operator": "THTR",
|
||||
"operator_name": "Thames Travel",
|
||||
"source": "Traveline timetable (not a nextbuses live region)"
|
||||
}
|
||||
],
|
||||
"X32": [{
|
||||
"aimed_departure_time": "10:09",
|
||||
"best_departure_estimate": "10:09",
|
||||
"date": "2017-05-09",
|
||||
"dir": "inbound",
|
||||
"direction": "Parkway Station (Didcot)",
|
||||
"expected_departure_date": null,
|
||||
"expected_departure_time": null,
|
||||
"id": "https://transportapi.com/v3/uk/bus/route/THTR/X32/inbound/340000368SHE/2017-05-09/10:09/timetable.json?app_id=e99&app_key=058",
|
||||
"line": "X32",
|
||||
"line_name": "X32",
|
||||
"mode": "bus",
|
||||
"operator": "THTR",
|
||||
"operator_name": "Thames Travel",
|
||||
"source": "Traveline timetable (not a nextbuses live region)"
|
||||
},
|
||||
{
|
||||
"aimed_departure_time": "10:30",
|
||||
"best_departure_estimate": "10:30",
|
||||
"date": "2017-05-09",
|
||||
"dir": "inbound",
|
||||
"direction": "Parks Road (Oxford City Centre)",
|
||||
"expected_departure_date": null,
|
||||
"expected_departure_time": null,
|
||||
"id": "https://transportapi.com/v3/uk/bus/route/THTR/X32/inbound/340000368SHE/2017-05-09/10:30/timetable.json?app_id=e99&app_key=058",
|
||||
"line": "X32",
|
||||
"line_name": "X32",
|
||||
"mode": "bus",
|
||||
"operator": "THTR",
|
||||
"operator_name": "Thames Travel",
|
||||
"source": "Traveline timetable (not a nextbuses live region)"
|
||||
},
|
||||
{
|
||||
"aimed_departure_time": "10:39",
|
||||
"best_departure_estimate": "10:39",
|
||||
"date": "2017-05-09",
|
||||
"dir": "inbound",
|
||||
"direction": "Parkway Station (Didcot)",
|
||||
"expected_departure_date": null,
|
||||
"expected_departure_time": null,
|
||||
"id": "https://transportapi.com/v3/uk/bus/route/THTR/X32/inbound/340000368SHE/2017-05-09/10:39/timetable.json?app_id=e99&app_key=058",
|
||||
"line": "X32",
|
||||
"line_name": "X32",
|
||||
"mode": "bus",
|
||||
"operator": "THTR",
|
||||
"operator_name": "Thames Travel",
|
||||
"source": "Traveline timetable (not a nextbuses live region)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"indicator": "in",
|
||||
"locality": "Harwell Campus",
|
||||
"name": "Bus Station (in)",
|
||||
"request_time": "2017-05-09T10:03:41+01:00",
|
||||
"smscode": "oxfajwgp",
|
||||
"stop_name": "Bus Station"
|
||||
}
|
511
tests/fixtures/uk_transport_train.json
vendored
Normal file
511
tests/fixtures/uk_transport_train.json
vendored
Normal file
@ -0,0 +1,511 @@
|
||||
{
|
||||
"date": "2017-07-10",
|
||||
"time_of_day": "06:10",
|
||||
"request_time": "2017-07-10T06:10:05+01:00",
|
||||
"station_name": "Wimbledon",
|
||||
"station_code": "WIM",
|
||||
"departures": {
|
||||
"all": [
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24671405",
|
||||
"train_uid": "W36814",
|
||||
"platform": "8",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:13",
|
||||
"aimed_arrival_time": null,
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Wimbledon",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "STARTS HERE",
|
||||
"expected_arrival_time": null,
|
||||
"expected_departure_time": "06:13",
|
||||
"best_arrival_estimate_mins": null,
|
||||
"best_departure_estimate_mins": 2
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673205",
|
||||
"train_uid": "W36613",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:14",
|
||||
"aimed_arrival_time": "06:13",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Hampton Court",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "EARLY",
|
||||
"expected_arrival_time": "06:13",
|
||||
"expected_departure_time": "06:14",
|
||||
"best_arrival_estimate_mins": 2,
|
||||
"best_departure_estimate_mins": 3
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673505",
|
||||
"train_uid": "W36012",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:20",
|
||||
"aimed_arrival_time": "06:20",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Guildford",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:20",
|
||||
"expected_departure_time": "06:20",
|
||||
"best_arrival_estimate_mins": 9,
|
||||
"best_departure_estimate_mins": 9
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673305",
|
||||
"train_uid": "W34087",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:23",
|
||||
"aimed_arrival_time": "06:23",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Dorking",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "XX",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:23",
|
||||
"expected_departure_time": "06:23",
|
||||
"best_arrival_estimate_mins": 12,
|
||||
"best_departure_estimate_mins": 12
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24671505",
|
||||
"train_uid": "W37471",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:32",
|
||||
"aimed_arrival_time": "06:31",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "London Waterloo",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:31",
|
||||
"expected_departure_time": "06:32",
|
||||
"best_arrival_estimate_mins": 20,
|
||||
"best_departure_estimate_mins": 21
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673605",
|
||||
"train_uid": "W35790",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:35",
|
||||
"aimed_arrival_time": "06:35",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Woking",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:35",
|
||||
"expected_departure_time": "06:35",
|
||||
"best_arrival_estimate_mins": 24,
|
||||
"best_departure_estimate_mins": 24
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673705",
|
||||
"train_uid": "W35665",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:38",
|
||||
"aimed_arrival_time": "06:38",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Epsom",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:38",
|
||||
"expected_departure_time": "06:38",
|
||||
"best_arrival_estimate_mins": 27,
|
||||
"best_departure_estimate_mins": 27
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24671405",
|
||||
"train_uid": "W36816",
|
||||
"platform": "8",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:43",
|
||||
"aimed_arrival_time": "06:43",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "London Waterloo",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:43",
|
||||
"expected_departure_time": "06:43",
|
||||
"best_arrival_estimate_mins": 32,
|
||||
"best_departure_estimate_mins": 32
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673205",
|
||||
"train_uid": "W36618",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:44",
|
||||
"aimed_arrival_time": "06:43",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Hampton Court",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:43",
|
||||
"expected_departure_time": "06:44",
|
||||
"best_arrival_estimate_mins": 32,
|
||||
"best_departure_estimate_mins": 33
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673105",
|
||||
"train_uid": "W36429",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:47",
|
||||
"aimed_arrival_time": "06:46",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Shepperton",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:46",
|
||||
"expected_departure_time": "06:47",
|
||||
"best_arrival_estimate_mins": 35,
|
||||
"best_departure_estimate_mins": 36
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24629204",
|
||||
"train_uid": "W36916",
|
||||
"platform": "6",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:47",
|
||||
"aimed_arrival_time": "06:47",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Basingstoke",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "LATE",
|
||||
"expected_arrival_time": "06:48",
|
||||
"expected_departure_time": "06:48",
|
||||
"best_arrival_estimate_mins": 37,
|
||||
"best_departure_estimate_mins": 37
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673505",
|
||||
"train_uid": "W36016",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:50",
|
||||
"aimed_arrival_time": "06:49",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Guildford",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:49",
|
||||
"expected_departure_time": "06:50",
|
||||
"best_arrival_estimate_mins": 38,
|
||||
"best_departure_estimate_mins": 39
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673705",
|
||||
"train_uid": "W35489",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:53",
|
||||
"aimed_arrival_time": "06:52",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Guildford",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "EARLY",
|
||||
"expected_arrival_time": "06:52",
|
||||
"expected_departure_time": "06:53",
|
||||
"best_arrival_estimate_mins": 41,
|
||||
"best_departure_estimate_mins": 42
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673405",
|
||||
"train_uid": "W37107",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "06:58",
|
||||
"aimed_arrival_time": "06:57",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Chessington South",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "06:57",
|
||||
"expected_departure_time": "06:58",
|
||||
"best_arrival_estimate_mins": 46,
|
||||
"best_departure_estimate_mins": 47
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24671505",
|
||||
"train_uid": "W37473",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:02",
|
||||
"aimed_arrival_time": "07:01",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "London Waterloo",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "EARLY",
|
||||
"expected_arrival_time": "07:01",
|
||||
"expected_departure_time": "07:02",
|
||||
"best_arrival_estimate_mins": 50,
|
||||
"best_departure_estimate_mins": 51
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673605",
|
||||
"train_uid": "W35795",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:05",
|
||||
"aimed_arrival_time": "07:04",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Woking",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:04",
|
||||
"expected_departure_time": "07:05",
|
||||
"best_arrival_estimate_mins": 53,
|
||||
"best_departure_estimate_mins": 54
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673305",
|
||||
"train_uid": "W34090",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:08",
|
||||
"aimed_arrival_time": "07:07",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Dorking",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "XX",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:07",
|
||||
"expected_departure_time": "07:08",
|
||||
"best_arrival_estimate_mins": 56,
|
||||
"best_departure_estimate_mins": 57
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673205",
|
||||
"train_uid": "W36623",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:13",
|
||||
"aimed_arrival_time": "07:12",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Hampton Court",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:12",
|
||||
"expected_departure_time": "07:13",
|
||||
"best_arrival_estimate_mins": 61,
|
||||
"best_departure_estimate_mins": 62
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24671405",
|
||||
"train_uid": "W36819",
|
||||
"platform": "8",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:13",
|
||||
"aimed_arrival_time": "07:13",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "London Waterloo",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:13",
|
||||
"expected_departure_time": "07:13",
|
||||
"best_arrival_estimate_mins": 62,
|
||||
"best_departure_estimate_mins": 62
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673105",
|
||||
"train_uid": "W36434",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:16",
|
||||
"aimed_arrival_time": "07:15",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Shepperton",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:15",
|
||||
"expected_departure_time": "07:16",
|
||||
"best_arrival_estimate_mins": 64,
|
||||
"best_departure_estimate_mins": 65
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673505",
|
||||
"train_uid": "W36019",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:19",
|
||||
"aimed_arrival_time": "07:18",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Guildford",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:18",
|
||||
"expected_departure_time": "07:19",
|
||||
"best_arrival_estimate_mins": 67,
|
||||
"best_departure_estimate_mins": 68
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673705",
|
||||
"train_uid": "W35494",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:22",
|
||||
"aimed_arrival_time": "07:21",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Guildford",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:21",
|
||||
"expected_departure_time": "07:22",
|
||||
"best_arrival_estimate_mins": 70,
|
||||
"best_departure_estimate_mins": 71
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673205",
|
||||
"train_uid": "W36810",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:25",
|
||||
"aimed_arrival_time": "07:24",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Esher",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:24",
|
||||
"expected_departure_time": "07:25",
|
||||
"best_arrival_estimate_mins": 73,
|
||||
"best_departure_estimate_mins": 74
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24673405",
|
||||
"train_uid": "W37112",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:28",
|
||||
"aimed_arrival_time": "07:27",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "Chessington South",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:27",
|
||||
"expected_departure_time": "07:28",
|
||||
"best_arrival_estimate_mins": 76,
|
||||
"best_departure_estimate_mins": 77
|
||||
},
|
||||
{
|
||||
"mode": "train",
|
||||
"service": "24671505",
|
||||
"train_uid": "W37476",
|
||||
"platform": "5",
|
||||
"operator": "SW",
|
||||
"operator_name": "South West Trains",
|
||||
"aimed_departure_time": "07:32",
|
||||
"aimed_arrival_time": "07:31",
|
||||
"aimed_pass_time": null,
|
||||
"origin_name": "London Waterloo",
|
||||
"source": "Network Rail",
|
||||
"destination_name": "London Waterloo",
|
||||
"category": "OO",
|
||||
"status": "ON TIME",
|
||||
"expected_arrival_time": "07:31",
|
||||
"expected_departure_time": "07:32",
|
||||
"best_arrival_estimate_mins": 80,
|
||||
"best_departure_estimate_mins": 81
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user