A modern glass-style Lovelace card for Home Assistant displaying real-time S-Bahn, Tram, and Bus departures. The card reads departures from Home Assistant entity attributes — it makes no transit-API calls of its own. A single, rate-controlled poll lives on the HA side (a REST sensor), so the card simply renders whatever your entities expose.
v2 — breaking change. Earlier versions fetched the Deutsche Bahn API directly from the browser. Deutsche Bahn retired the HAFAS backend and the public
v6.db.transport.restnow shares a tiny rate limit (and frequently returns 503), which a browser client cannot honor. The card now sources data from HA entities instead. See Migrating from v1.
- Departures sourced from Home Assistant entities (zero browser-side API calls, no CORS proxies)
- Reactive: re-renders the moment HA pushes new entity state — no polling timer in the card
- Glass/blur aesthetic with dark theme
- Collapsible sections for S-Bahn / Tram / Bus
- Live countdown updates (local 1-second clock)
- Render-time freshness filtering (hides cancelled / past / implausibly-delayed entries between server polls)
- Delay indicators (+3, +5), platform display (Gl. 2), stale-data indicator
- List or 2-column map layout
- Open HACS → three-dots menu → Custom repositories
- Add this repository URL with category Lovelace
- Search for "Transit Card", install, and restart Home Assistant
- Download
transit-card.jsfrom the latest release - Copy to
config/www/transit-card.js - Add a resource in Settings → Dashboards → Resources: URL
/local/transit-card.js, type JavaScript Module
Each transit type points at one Home Assistant entity whose departures attribute holds the list (see the data contract).
type: custom:transit-card
title: Abfahrten
stops:
sbahn: sensor.transit_s_bahn_all # string shorthand
tram: { entity: sensor.transit_tram_all } # or object form
bus: sensor.transit_bus_all
max_departures: 5
collapsed:
- bus
style: glasstype: custom:transit-card
title: Weststadt Transit
layout: map
stops:
sbahn: sensor.transit_s_bahn_all
tram: sensor.transit_tram_all
map:
center: { lat: 49.3983, lng: 8.6887 }
# Optional per-station coords (keyed by the payload's stopName) when the
# payload itself carries no lat/lng:
stops:
Christuskirche: { lat: 49.40168, lng: 8.685209 }
max_departures: 3Map markers and direction arrows come from configuration and/or optional payload fields (lat/lng/directionArrow) — the card performs no /locations lookups.
Only configure the types you need — omit a type to hide its section.
| Option | Type | Default | Description |
|---|---|---|---|
title |
string | – | Optional card header |
layout |
string | list |
list or map |
stops.sbahn |
entity / {entity} |
– | Source entity for S-Bahn. Omit to hide. |
stops.tram |
entity / {entity} |
– | Source entity for Tram. Omit to hide. |
stops.bus |
entity / {entity} |
– | Source entity for Bus. Omit to hide. |
max_departures |
number | 5 | Max departures per section |
collapsed |
array | [] |
Sections to start collapsed (sbahn, tram, bus) |
style |
string | glass |
glass (blur) or solid |
stale_threshold |
number | 180 | Seconds before the newest last_updated is flagged stale |
map.zoom |
number | auto | Map zoom (layout map) |
map.center |
object | auto | Map center { lat, lng } (layout map) |
map.stops |
object | – | Per-station { lat, lng }, keyed by stopName (layout map) |
The card reads attributes.departures from each source entity. Each element:
| Field | Type | Notes |
|---|---|---|
line |
string | e.g. "S3" (falls back to "?") |
direction |
string | destination headsign |
planned |
string (ISO 8601) | scheduled departure |
when |
string | null | realtime departure; null = no realtime data |
delay |
number | minutes (normalize server-side; 0/absent = on time) |
platform |
string | null | e.g. "2" → rendered as Gl. 2 |
cancelled |
boolean | default false |
id |
string | optional, stable key |
directionArrow |
'left'|'right'|null |
optional, for the map/list arrow |
stopName |
string | optional, used to group map markers |
lat / lng |
number | optional, map marker coordinates |
The countdown is computed from when ?? planned. Unknown fields are ignored; missing optional fields degrade gracefully.
Because the public DB endpoint is rate-limited/unreliable, run your own
db-vendo-client and have HA poll it once per minute.
1. Run db-vendo-client (Docker; on a Proxmox LXC use --network host to avoid the unprivileged-port sysctl error):
docker run -d --name dbvendo --restart unless-stopped --network host \
-e USER_AGENT="my-home-assistant (contact)" \
-e DB_PROFILE=dbnav \
ghcr.io/public-transport/db-vendo-client
# -> http://<host>:3000/stops/<id>/departures2. REST sensor — one poll/min into a _raw entity:
# configuration.yaml
rest:
- resource: "http://<DBVENDO_HOST>:3000/stops/8002681/departures"
scan_interval: 60
params: { duration: 60, results: 8 }
sensor:
- name: "Transit S-Bahn Raw"
unique_id: transit_s_bahn_raw
value_template: "{{ (value_json.departures | default([])) | count }}"
json_attributes: [departures]3. Template sensor — normalize raw FPTF into the card contract. Filter on line.product (the ?suburban=&tram=&bus= query params are unreliable on the dbnav profile) and convert delay from seconds → minutes:
template:
- sensor:
- name: "Transit S-Bahn All"
unique_id: transit_s_bahn_all
state: >
{{ (state_attr('sensor.transit_s_bahn_raw','departures') | default([]))
| selectattr('line.product','eq','suburban') | list | count }}
attributes:
departures: >
{% set raw = state_attr('sensor.transit_s_bahn_raw','departures') | default([]) %}
{% set ns = namespace(items=[]) %}
{% for d in raw if d.line.product == 'suburban' %}
{% set ns.items = ns.items + [{
'line': d.line.name,
'direction': d.direction,
'planned': d.plannedWhen,
'when': d.when,
'delay': (d.delay // 60) if d.delay is not none else 0,
'platform': d.platform,
'cancelled': d.cancelled | default(false),
'stopName': d.stop.name | default('')
}] %}
{% endfor %}
{{ ns.items }}Then point the card at sensor.transit_s_bahn_all. Repeat per type, changing the stop ID and the line.product filter (suburban / tram / bus).
Aggregating multiple stops into one type (e.g. several tram stops): give each stop its own _raw REST sensor and merge them in the template (concatenate the per-stop lists before the loop), or query each and combine. Keep the merged list capped (results: 8) and consider excluding the _raw/_all sensors from the recorder to avoid DB growth:
recorder:
exclude:
entity_globs:
- sensor.transit_*_rawstops.{sbahn,tram,bus}change from DB stop-ID strings/arrays to HA entity references. The card throws a clearsetConfigerror if it detects an old numeric stop ID.proxy_urlandrefresh_intervalare removed (the card no longer fetches anything; HA controls the poll cadence).- Set up the HA sensors above, then update your card YAML to reference the entities.
| Symptom | Check |
|---|---|
| "Keine Abfahrten gefunden" | The source entity exists and its departures attribute is a non-empty list (Developer Tools → States). |
| Status shows Fehler | The configured entity is unavailable/unknown/missing, or the REST sensor isn't producing data. Check the db-vendo-client container and the REST sensor. |
| Status shows Veraltet (stale) | The newest last_updated is older than stale_threshold — the server-side poll has stopped (container down / DB outage). |
| Wrong modes in a section | Filter by line.product in the template (a multi-modal stop returns S-Bahn + tram + bus together). |
setConfig error about a stop ID |
You still have v1 numeric stop IDs in stops.*; switch to entity references. |
MIT
- Transit data via db-vendo-client (DB APIs)
- Inspired by real German transit departure boards