Skip to content

Repository files navigation

ÖBB Departures

Live train and transit departure boards for Home Assistant, powered by ÖBB's own HAFAS backend.

HACS Custom GitHub release License: MIT Home Assistant

Search for any Austrian station, optionally narrow it down to a specific line, direction, or stop along the way, and get real-time-adjusted departure sensors — no YAML required.

Example rendering: a cancelled departure and a delayed departure

Important

Unofficial integration. This talks to a reverse-engineered endpoint with no public developer program or SLA from ÖBB. See Unofficial API for what that means in practice.


Contents

Features

  • UI-based setup — search for your station and go, no YAML editing.
  • Per-station filters — track a specific commute (e.g. "S50 toward Wien Westbahnhof, passing through Weidlingau") separately from a station's full board:
    • Line / train name
    • Direction (destination text)
    • Intermediate stop
    • Transport type (IC/EC, REX/R, S-Bahn, Bus, U-Bahn, Tram, Ship, on-call transit, high-speed, rail-replacement bus, car-carrying trains)
  • Two sensors per configured board:
    Sensor What it gives you
    Next departure Timestamp sensor (shows "in 12 minutes" etc.) with the full upcoming board as an attribute
    Departures count How many upcoming departures currently match your filters
  • Configurable polling interval — default 90 seconds, adjustable per station.

Installation

Via HACS (custom repository)

  1. In Home Assistant: HACS → Integrations → ⋮ → Custom repositories.
  2. Add https://github.com/agentcny/ha-oebb as an Integration.
  3. Find ÖBB Departures in HACS and install it.
  4. Restart Home Assistant.

Manual

  1. Copy custom_components/oebb_hafas/ into your Home Assistant config/custom_components/ directory.
  2. Restart Home Assistant.

Configuration

  1. Settings → Devices & services → Add integration, search for ÖBB Departures.

  2. Enter part of a station name (e.g. Weidlingau) and pick the correct match from the search results.

  3. Optionally set filters:

    Filter Matches against Example
    Line filter Line/train name (comma-separated) S50, S45
    Direction filter Journey's destination text Wien Westbahnhof
    Intermediate stop filter Journey's full route Hütteldorf
    Transport types Which kinds of services to include
    Update interval Poll frequency in seconds (default 90, min 30) 90
  4. Submit — this creates one device with two sensors for that station/filter combination.

Need departures for the same station in a different direction, or a different station entirely? Add the integration again — each config entry is independent. Filters and the update interval can be changed later from the integration's Configure option, without re-adding it.

Entities

Entity Description
sensor.<name>_next_departure Timestamp of the next matching departure (real-time-adjusted). Attributes include direction, line, category, delay_minutes, cancelled, intermediate_stops, and the full departures list.
sensor.<name>_departures_count Number of upcoming departures currently matching your filters.
Example attributes

sensor.<name>_next_departure mirrors the next departure's own fields at the top level, plus the full board in departures:

time: 2026-08-09T17:51:00+02:00
scheduled_time: 2026-08-09T17:48:00+02:00
realtime_time: 2026-08-09T17:51:00+02:00
delay_minutes: 3
direction: Wien Westbahnhof
line: S 50
category: S-Bahn
cancelled: false
intermediate_stops: Wien Weidlingau Bahnhst, Wien Hadersdorf Bahnhst (17:50), Wien Wolf in der Au Bahnhst (17:52), Wien Hütteldorf Bahnhof (17:55), Wien Penzing Bahnhof (17:59), Wien Westbahnhof (18:03)
departures:
  - time: '2026-08-09T17:51:00+02:00'
    scheduled_time: '2026-08-09T17:48:00+02:00'
    realtime_time: '2026-08-09T17:51:00+02:00'
    delay_minutes: 3
    direction: Wien Westbahnhof
    line: S 50
    category: S-Bahn
    cancelled: false
    intermediate_stops:
      - Wien Weidlingau Bahnhst
      - Wien Hadersdorf Bahnhst (17:50)
      - Wien Wolf in der Au Bahnhst (17:52)
      - Wien Hütteldorf Bahnhof (17:55)
      - Wien Penzing Bahnhof (17:59)
      - Wien Westbahnhof (18:03)
  - time: '2026-08-09T18:03:00+02:00'
    scheduled_time: '2026-08-09T18:03:00+02:00'
    realtime_time: '2026-08-09T18:03:00+02:00'
    delay_minutes: 0
    direction: Wien Westbahnhof
    line: S 50
    category: S-Bahn
    cancelled: false
    intermediate_stops:
      - Wien Weidlingau Bahnhst
      - Wien Hadersdorf Bahnhst (18:05)
      - Wien Wolf in der Au Bahnhst (18:07)
      - Wien Hütteldorf Bahnhof (18:10)
      - Wien Penzing Bahnhof (18:14)
      - Wien Westbahnhof (18:18)
Example Lovelace card
type: entities
title: Next train
entities:
  - entity: sensor.weidlingau_next_departure
  - entity: sensor.weidlingau_departures_count
Example: custom button-card per departure

The departures attribute on the next_departure sensor is a list of upcoming journeys, so a template card like button-card can render one row per departure, with the delay/cancellation styling handled in the template. variables.index picks which entry in the list to show — duplicate the card with index: 0, index: 1, etc. for a full board.

type: custom:button-card
entity: sensor.weidlingau_to_westbahnhof_next_departure
variables:
  index: 1
name: |
  [[[
    const departure = entity.attributes.departures[variables.index];
    return departure.direction
        .replace(/Wien\s+/g, '').replace(/\s*\(.*\)/g, '').replace(/Bahnhof/g, '').trim();
  ]]]
icon: |
  [[[
    const cat = entity.attributes.departures[variables.index].category;
    return cat === 'BUS'
      ? "fapro-duotone:bus"
      : "fapro-duotone:train";
  ]]]
styles:
  card:
    - padding: 12px
  grid:
    - grid-template-areas: '"i t b" "i n n"'
    - grid-template-columns: min-content
    - column-gap: 10px
  icon:
    - width: 30px
    - color: |
        [[[
          const cat = entity.attributes.departures[variables.index].category;
          return cat === 'BUS'
            ? "var(--color-green)"
            : "var(--color-blue)";
        ]]]
  img_cell:
    - width: 30px
  name:
    - justify-self: start
    - font-size: 12px
    - font-weight: 600
    - color: var(--subtext-color)
  custom_fields:
    t:
      - justify-self: start
      - align-self: center
      - font-size: 14px
    b:
      - justify-self: end
      - align-self: center
      - font-size: 10px
      - font-weight: bold
      - padding: 2px 8px
      - border-radius: 12px
      - background-color: |
          [[[
            const cat = entity.attributes.departures[variables.index].category;
            return cat === 'BUS'
              ? "var(--color-green)"
              : "var(--color-blue)";
          ]]]
      - color: white
      - text-transform: uppercase
custom_fields:
  t: |
    [[[
      const departure = entity.attributes.departures[variables.index];
      const hhmm = (iso) => iso.split('T')[1].slice(0, 5);
      if (departure.cancelled) {
          // Cancelled: show the scheduled time struck through in red
          return `<span style="color: var(--color-red); text-decoration: line-through;">${hhmm(departure.scheduled_time)}</span>`;
      } else if (departure.delay_minutes) {
          // Delayed: strike through the scheduled time, show the real-time one in red
          return `<s>${hhmm(departure.scheduled_time)}</s> <span style="color: var(--color-red); font-weight: 600;">${hhmm(departure.realtime_time)}</span>`;
      } else {
          // On time: just show the scheduled time
          return hhmm(departure.scheduled_time);
      }
    ]]]
  b: |
    [[[
      const departure = entity.attributes.departures[variables.index];
      return departure.line;
    ]]]

Troubleshooting

Sensor shows "unavailable"

The integration couldn't reach fahrplan.oebb.at, or it returned an error (e.g. an invalid station). This is usually transient — it recovers automatically once the endpoint is reachable again, and Home Assistant only logs the failure once (not on every retry) so your log won't get spammed.

No departures match my filter

Double-check the filter text against what actually appears in the unfiltered board — line names, directions, and stop names must match ÖBB's own wording (e.g. "Wien Westbahnhof", not "Vienna West").

It stopped working after previously working fine

ÖBB occasionally changes internal details of this unofficial endpoint (see Unofficial API below) — check the issue tracker for a known fix, or open a new issue with your Home Assistant log output.

Unofficial API

fahrplan.oebb.at's HAFAS backend is not a published, versioned API — it's the same backend ÖBB's own website calls, reverse-engineered for this integration. That means:

  • No SLA or rate-limit documentation. The default 90-second poll interval is chosen to be reasonably light, not because ÖBB has published a required minimum.
  • Shared auth token. The client sends an authentication token that ÖBB's own frontend also sends to every visitor's browser — it isn't a personal/secret credential, but ÖBB could rotate it at any time, which would break this integration until it's updated here.
  • Undocumented response shapes. Parsing is defensive, but ÖBB changes could still break things.

Removing the integration

Settings → Devices & services, find the ÖBB Departures entry you want to remove, and delete it. This removes its device and sensors; no other cleanup is required.


MIT License · Made for the Home Assistant community

About

ÖBB Departures — live train/transit departure boards for Home Assistant

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages