Bringing Swiss Public Transport Departures to Grafana
de enUpdate (2020-11-27)
The API endpoint used here has been deprecated, and a new endpoint is available. The updated script can be found on Gitlab.
The Swiss Railways (SBB) provide a collection of static data sets and dynamic APIs at opentransportdata.swiss. One endpoint provides a list of departures or arrivals for a given train, bus or tram station.
In this blogpost, I'm showing you how I'm using this API to get a list of upcoming departures for the station next to my home, and how do get this list into Grafana.
The API
The XML API is documented in the "API Cookbook". A request looks like this:
POST /trias HTTP/1.1
Host: https://api.opentransportdata.swiss
Authorization: TOKEN
Content-Type: text/xml
<?xml version="1.0" encoding="UTF-8"?>
<Trias
version="1.1"
xmlns="http://www.vdv.de/trias"
xmlns:siri="http://www.siri.org.uk/siri"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ServiceRequest>
<siri:RequestTimestamp> NOW </siri:RequestTimestamp>
<siri:RequestorRef>EPSa</siri:RequestorRef>
<RequestPayload>
<StopEventRequest>
<Location>
<LocationRef>
<StopPointRef> BPUIC </StopPointRef>
</LocationRef>
<DepArrTime> NOW </DepArrTime>
</Location>
<Params>
<NumberOfResults> N_RESULTS </NumberOfResults>
<StopEventType>departure</StopEventType>
<IncludePreviousCalls>false</IncludePreviousCalls>
<IncludeOnwardCalls>false</IncludeOnwardCalls>
<IncludeRealtimeData>true</IncludeRealtimeData>
</Params>
</StopEventRequest>
</RequestPayload>
</ServiceRequest>
</Trias>
This request is fairly minimal; it is limited to a single station, and without further information such as previous and following stops. You only need to fill in the following arguments to make this work for your station of choice:
- TOKEN: API Token, need to register an account.
- NOW (2x): Current time in ISO-8601 form.
- BPUIC: Numeric ID of the station ("Betriebspunkt"), can be looked up in the DiDok dataset.
- N_RESULTS: Maximal number of results to return.
Prometheus Ingestion
XML is a bit... let's say, uncomfortable to handle in Bash scripts,
so I resorted to using the xsltproc
tool to transform the API
response into something easily iterable; the XSLT document i came up
with looks like this and generates CSV content:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:trias="http://www.vdv.de/trias">
<xsl:output method="text" />
<xsl:template match="/">
<xsl:for-each select="//trias:StopEvent">
<xsl:value-of select="trias:Service/trias:PublishedLineName/trias:Text"/>
<xsl:text>;</xsl:text>
<xsl:value-of select="trias:Service/trias:DestinationText/trias:Text"/>
<xsl:text>;</xsl:text>
<xsl:value-of select="trias:ThisCall/trias:CallAtStop/trias:ServiceDeparture/trias:TimetabledTime"/>
<xsl:text>;</xsl:text>
<xsl:value-of select="trias:ThisCall/trias:CallAtStop/trias:ServiceDeparture/trias:EstimatedTime"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Each line in the result represents a stop at the station, with the following fields:
- Number of the train or bus line
- Name of the destination
- Scheduled departure time
- Estimated/actual departure time
This format is quite easy to handle in Bash; let's parse the ISO-8601 timestamps, compute the delay for each stop and then emit the results in Prometheus collector format:
# TYPE sbb_station_departure gauge
# HELP sbb_station_departure Departures from a train or bus station
# TYPE sbb_station_delay gauge
# HELP sbb_station_delay Departure delay
sbb_station_departure{line="26",planned="1580875380000",destination="Erstfeld"} 1580875380000
sbb_station_delay{line="26",planned="1580875380000",destination="Erstfeld"} 0
sbb_station_departure{line="36",planned="1580875980000",destination="Zürich HB"} 1580875980000
sbb_station_delay{line="36",planned="1580875980000",destination="Zürich HB"} 0
I'm using the Textfile Collector feature of the Prometheus Node Exporter to ingest this document into Prometheus.
Display in Grafana
I'm showing this data in a table panel in Grafana, using two queries: one for the scheduled departure, one for the delay. Here, you can filter the departures by destination, if not already done in your script:
min without (__name__) (sbb_station_departure{destination=~".*Zürich.*"})
and
min without (__name__) (sbb_station_delay{destination=~".*Zürich.*"})
And finally, after some styling, the result looks like this:
The code can be found on Gitlab.