Live train and transit departure boards for Home Assistant, powered by ÖBB's own HAFAS backend.
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.
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.
- Features
- Installation
- Configuration
- Entities
- Troubleshooting
- Unofficial API
- Removing the integration
- 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.
- In Home Assistant: HACS → Integrations → ⋮ → Custom repositories.
- Add
https://github.com/agentcny/ha-oebbas an Integration. - Find ÖBB Departures in HACS and install it.
- Restart Home Assistant.
- Copy
custom_components/oebb_hafas/into your Home Assistantconfig/custom_components/directory. - Restart Home Assistant.
-
Settings → Devices & services → Add integration, search for ÖBB Departures.
-
Enter part of a station name (e.g.
Weidlingau) and pick the correct match from the search results. -
Optionally set filters:
Filter Matches against Example Line filter Line/train name (comma-separated) S50, S45Direction filter Journey's destination text Wien WestbahnhofIntermediate stop filter Journey's full route HütteldorfTransport types Which kinds of services to include — Update interval Poll frequency in seconds (default 90, min 30) 90 -
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.
| 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_countExample: 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;
]]]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.
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.
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
