# name
Bringing Swiss Public Transport Departures to Grafana

# nameMap.en
Bringing Swiss Public Transport Departures to Grafana

# content
Update (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>&#x0A;</xsl:text>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Each line in the result represents a stop at the station, with the following fields:

1. Number of the train or bus line

2. Name of the destination

3. Scheduled departure time

4. 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.

# contentMap.de
Update (2020-11-27)

Der hier verwendete API-Endpunkt ist veraltet und wurde durch einen neuen Endpunkt ersetzt. Das aktualisierte Skript liegt auf Gitlab.

Die SBB bieten unter opentransportdata.swiss eine Sammlung an statischen Datensätzen und dynamischen APIs an. Ein Endpunkt bietet eine Liste an Zug-, Bus- oder Tramabfahrten oder -Ankünften für gewisse Haltestellen an.

In diesem Blogpost erkläre ich, wie ich diese API verwende, um eine Liste an kommenden Abfahrten ab meinem "Heimatbahnhof" zu erhalten, und wie diese Liste in Grafana landet.

Die API

Die XML-API ist im "API Cookbook" dokumentiert. Eine Anfrage an die API sieht etwas so aus:

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>

Diese Anfrage ist sehr minimalistisch; sie beschränkt sich auf eine einzelne Haltestelle, und ignoriert weitere Informationen wie Zwischenhalte. Hier muss man einfach nur die folgenden Argumente einfügen, um die API für die "eigene" Haltestelle zu verwenden:

TOKEN: Das API Token, man muss einen Benutzeraccount anlegen.

NOW (2x): Aktueller Zeitpunkt in ISO-8601-Form.

BPUIC: Numerische ID der Haltestelle ("Betriebspunkt"), kann im DiDok-Datensatz nachgeschlagen werden.

N_RESULTS: Maximale Anzahl an zurückgegebenen Ergebnissen.

Daten ins Prometheus bekommen

XML ist ein bisschen... mühsam in Bash-Scripten zu verwenden, daher habe ich hier das xsltproc-Tool zur Hilfe genommen, um die Antwort der API in ein handlicheres Format zu übersetzen; das XSLT-Dokument, das hierfür entstanden ist, sieht so aus und generiert eine Ausgabe in CSV-Form:

<?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>&#x0A;</xsl:text>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Jede Zeile der Ausgabe repräsentiert einen Halt an der Haltestelle, mit den folgenden Datenfeldern:

1. Nummer der Bus- oder Zuglinie

2. Name des Fahrtziels

3. Fahrplanmässige Abfahrtszeit

4. Tatsächliche/geschätzte Abfahrtszeit

Dieses Format lässt sich in Bash deutlich einfacher verarbeiten; für jeden Halt werden die ISO-8601-Timestamps geparst, daraus die Verspätung berechnet, und das Ergebnis im Prometheus-Collector-Format ausgegeben:

# 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

Um diese Daten in Prometheus zu importieren, verwende ich das Textfile Collector-Feature des Prometheus Node Exporter.

Anzeige in Grafana

Die Abfahrtsdaten lassen sich in Grafana in einer Tabelle darstellen. Hierzu werden zwei Prometheus-Queries benötigt; eine für die planmässige Abfahrt, eine für die Verspätung. Wenn man nach bestimmten Fahrtzielen filtern möchte, lässt sich dies zum Beispiel hier tun:

min without (__name__) (sbb_station_departure{destination=~".*Zürich.*"})

und

min without (__name__) (sbb_station_delay{destination=~".*Zürich.*"})

Nach ein bisschen Styling sieht das ganze dann so aus:

Der Code hierzu ist auf Gitlab zu finden.

# contentMap.en
Update (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>&#x0A;</xsl:text>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Each line in the result represents a stop at the station, with the following fields:

1. Number of the train or bus line

2. Name of the destination

3. Scheduled departure time

4. 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.