Using Shortcuts to Check Santander Cycles
TfL's Santander Cycles have been my favorite way to get around central London since I first tried them during Covid lockdowns. The annual subscription (with a student discount!) means I can make short journeys faster than on a bus, sometimes even faster than on the tube. But I had one problem when making the journey into LSE – often I would arrive to find every docking station completely full, meaning I had to pull out my phone to search for the nearest available docking space. That's not the most elegant thing to do when you're pulled over on the side of a busy road straddling a bike, especially when the app turns out to be frozen. I wanted to find an easier way to check for empty spaces near my destination.
In this post I show how to get the number of bikes and empty docks at any station using the TfL API and then show the steps to do this in the Shortcuts app. The Shortcut can be run from the Shortcuts app, from your iPhone or iPad's home screen as a standalone app or within a widget, from Siri, from your Mac's task bar, or even from a complication on your Apple Watch face. If you'd like to use these shortcuts yourself, here are the iCloud sharing links:
The TfL API
TfL has developed a unified API that provides real-time information about its services to anyone with the appropriate programming know-how. The API provides a BikePoint
endpoint that returns information about all docking stations. Let's pull this information and see what the JSON data for a single bikepoint looks like:
using HTTP, JSON3
r = HTTP.get("https://api.tfl.gov.uk/BikePoint")
bikepoints = r.body |> String |> JSON3.read
bp = first(bikepoints)
JSON3.pretty(bp)
{
"$type": "Tfl.Api.Presentation.Entities.Place, Tfl.Api.Presentation.Entities",
"id": "BikePoints_1",
"url": "/Place/BikePoints_1",
"commonName": "River Street , Clerkenwell",
"placeType": "BikePoint",
"additionalProperties": [
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "TerminalName",
"sourceSystemKey": "BikePoints",
"value": "001023",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "Installed",
"sourceSystemKey": "BikePoints",
"value": "true",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "Locked",
"sourceSystemKey": "BikePoints",
"value": "false",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "InstallDate",
"sourceSystemKey": "BikePoints",
"value": "1278947280000",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "RemovalDate",
"sourceSystemKey": "BikePoints",
"value": "",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "Temporary",
"sourceSystemKey": "BikePoints",
"value": "false",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "NbBikes",
"sourceSystemKey": "BikePoints",
"value": "7",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "NbEmptyDocks",
"sourceSystemKey": "BikePoints",
"value": "12",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "NbDocks",
"sourceSystemKey": "BikePoints",
"value": "19",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "NbStandardBikes",
"sourceSystemKey": "BikePoints",
"value": "7",
"modified": "2024-03-13T10:32:20.74Z"
},
{
"$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
"category": "Description",
"key": "NbEBikes",
"sourceSystemKey": "BikePoints",
"value": "0",
"modified": "2024-03-13T10:32:20.74Z"
}
],
"children": [
],
"childrenUrls": [
],
"lat": 51.529163,
"lon": -0.10997
}
There's a lot going on here, but the important things we observe are:
The unique
id
field will let us query the API for just this stationThe
commonName
field gives the name of the station as it appears in the appThe information we want (the number of bikes/E-bikes and empty docking points) live inside the
additionalProperties
field
The structure of the additionalProperties
field is a bit odd in that the keys are actually values themselves, so we can't access them by name. Let's get the index of each key-value pair:
for (i, property) in enumerate(bp.additionalProperties)
println("($i) ", property.key, " => ", property.value)
end
(1) TerminalName => 001023
(2) Installed => true
(3) Locked => false
(4) InstallDate => 1278947280000
(5) RemovalDate =>
(6) Temporary => false
(7) NbBikes => 7
(8) NbEmptyDocks => 12
(9) NbDocks => 19
(10) NbStandardBikes => 7
(11) NbEBikes => 0
So the indices we care about are [8, 10, 11]
.
function print_bike_point(bp)
println("$(bp.commonName) (id: $(bp.id))")
println("Number of standard bikes: $(bp.additionalProperties[10].value)")
println("Number of E-bikes: $(bp.additionalProperties[11].value)")
println("Number of empty docks: $(bp.additionalProperties[8].value)")
end
print_bike_point(bp)
River Street , Clerkenwell (id: BikePoints_1)
Number of standard bikes: 7
Number of E-bikes: 0
Number of empty docks: 12
Now that we know how to get the information we want for any station, we need to find the IDs of the stations we want to include in our search. The API provides a BikePoint/Search
endpoint where we can search stations by name:
r = HTTP.get(
"https://api.tfl.gov.uk/BikePoint/Search";
query=["query" => "Lincoln's Inn Fields"]
)
@show search_results = r.body |> String |> JSON3.read
search_results = (r.body |> String) |> JSON3.read = JSON3.Object[{
"$type": "Tfl.Api.Presentation.Entities.Place, Tfl.Api.Presentation.Entities",
"id": "BikePoints_809",
"url": "/Place/BikePoints_809",
"commonName": "Lincoln's Inn Fields, Holborn",
"placeType": "BikePoint",
"additionalProperties": [],
"children": [],
"childrenUrls": [],
"lat": 51.516277,
"lon": -0.118272
}]
Now that we have the ID, we can use the BikePoint/{id}
endpoint to request data for that bike point only:
base_url = "https://api.tfl.gov.uk/BikePoint/BikePoints_"
station_id = "809"
r = HTTP.get(base_url * station_id)
r.body |> String |> JSON3.read |> print_bike_point
Lincoln's Inn Fields, Holborn (id: BikePoints_809)
Number of standard bikes: 23
Number of E-bikes: 0
Number of empty docks: 5
Accessing the API through Shortcuts
Apple's Shortcuts app is able to read JSON objects as dictionaries, so with a little manipulation (and a lot of dragging and dropping) we can replicate the results of the above API call. I wanted to be able to run my Shortcut for multiple destinations (e.g. work, home, etc.), so I separated the Shortcut into two: an inner Shortcut to receive a list of stations and return a list of messages, and an outer Shortcut to pass a list of stations to the inner Shortcut and display the results as an alert.
The Inner Shortcut: Accessing the API
The steps to follow are:
Set the base URL
Add the station ID to the URL
Request the data from the API
Parse the response into a dictionary
Extract the station's
commonName
Extract the
additionalProperties
as a listGet the number of standard bikes at index 10
Get the number of E-bikes at index 11
Get the number of empty docks at index 8
Combine the results into text that can be displayed
Setting the Base URL
Constructing the API Call
We begin looping over every item in the list:
Parsing the API Response
Getting the Station Name
Getting the Station Properties
Because we want to access the additionalProperties
dictionary multiple times, we have to create a variable for it:
Scripting functionality in Shortcuts is fairly limited, so I had to hardcode the next three steps rather than using something like a loop:
Constructing the Output
Now I combine everything into a single message. I leave an empty line at the top so that when the Shortcut returns multiple stations they are separated by a space. I chose to represent each number with an emoji since it saves visual space, allowing all three numbers to fit into one line on my watch.
When you tell Siri to run the shortcut, the results are read aloud. This can sound pretty confusing with the output text I currently have written, but you can tweak the text output to make it sound more natural when spoken if you prefer to run the Shortcut through Siri.
The Outer Shortcut: Making a List of Stations
Constructing a list of stations you want to be included in the Shortcut can be a bit difficult, depending on how you approach it. I used the map in the mobile app to find the names of the stations I wanted to include and looked up the ID for each station. You can do this using the API, but I found it quicker to look up the XML feed and search for the station name using my browser (the number you need for the shortcut is the <id>
field, not <terminalName>
).
Once we've done that, the Shortcut itself is very simple:
Running the Shortcut produces a native alert:
The great advantage of this Shortcut is that these results are incredibly accessible because Shortcuts are built into iOS. You can add the Shortcut to your home screen as an icon, run it from a widget, access it from your Mac's task bar, or even run it from a complication on your watch face:
I use this Shortcut pretty much every time I leave the house, and it's saved me from riding up to a full station more times than I can count. If you'd like to try them out for yourself, here are the iCloud sharing links: