SBB-Abfahrtszeiten in Grafana anzeigen

de en

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:

Screenshot of a Grafana table panel with a train schedule

Der Code hierzu ist auf Gitlab zu finden.