140 lines
7.0 KiB
Markdown
140 lines
7.0 KiB
Markdown
# transit
|
|
|
|
"When's the next bus?" via voice, from [Phase 19 of the project plan](../docs/project-plan.md).
|
|
|
|
Not a trip planner — no transfers, no routing. Just: for one of the household's own
|
|
configured stops (Kennelbach's own stop(s), by default), what's leaving next. Data
|
|
comes from Vorarlberg's own published GTFS static schedule (VVV/VMOBIL), sourced via
|
|
Austria's national aggregator at mobilitaetsdaten.gv.at — real, existing
|
|
infrastructure this repo reads from, not something it generates.
|
|
|
|
- **`sync_gtfs.py`** — oneshot, weekly timer (`ENABLE_TRANSIT` in
|
|
`setup-container-host.sh`). Downloads the GTFS zip, keeps only the
|
|
stops/routes/trips near the household's configured stop(s)
|
|
(`GTFS_STOP_NAMES`), writes a small local SQLite DB. A full regional GTFS feed is
|
|
large and mostly irrelevant to one household; filtering at sync time is what
|
|
keeps lookups fast.
|
|
- **`server.py`** — always-on, bearer-token gated, `GET /departures?stop=<name>`
|
|
returns the next few departures computed from `calendar.txt`/`calendar_dates.txt`
|
|
(which services run today) joined against `stop_times.txt` for the matched
|
|
stop(s). **Published**, like `identity`/`pantry-vision` — `homeassistant` runs
|
|
`network_mode: host` in this stack and can't resolve plain container DNS names,
|
|
so a `rest_command` needs a real published port to reach this on, same reasoning
|
|
as `identity/identity.env.example`'s note about `HA_URL`.
|
|
|
|
## Route planning scope — "Austria, possibly global" has a real cost
|
|
|
|
`/plan` doesn't do any journey-planning itself — it proxies straight through to a
|
|
self-hosted **[OpenTripPlanner](https://www.opentripplanner.org/)** (OTP) instance,
|
|
because real A-to-B routing (transfers, walking legs, multi-modal itineraries) is a
|
|
mature, well-studied problem with good open-source engines already solving it, and
|
|
hand-rolling one here would be a bad trade.
|
|
|
|
**What OTP actually needs to answer a query is data loaded into its "graph": an OSM
|
|
map extract for the road/path network, plus every GTFS feed for the transit systems
|
|
you want it to route through.** That's the real scoping decision, not a code
|
|
feature:
|
|
|
|
- **Austria-wide** — one Austria OSM extract (a few hundred MB, e.g. from
|
|
[Geofabrik](https://download.geofabrik.de/europe/austria.html)) plus the GTFS
|
|
feeds for however many of Austria's regional operators you want covered
|
|
(`mobilitaetsdaten.gv.at`'s catalog lists them all, not just VVV) — a realistic,
|
|
moderate self-hosting commitment (single-digit GB of graph data, a build that
|
|
takes minutes, RAM in the low single-digit GB range for OTP itself).
|
|
- **Actually global** — every country's OSM data (the full planet extract is
|
|
**~80+ GB** compressed) plus GTFS for transit systems worldwide that publish one
|
|
at all (many don't, or only via a paid/restricted feed) — this is not a
|
|
"flip a flag" step up from Austria-wide, it's a full self-hosted mapping
|
|
infrastructure project with real disk/RAM/build-time cost, and results would still
|
|
be missing any transit system that has no public GTFS feed. **Start with Austria
|
|
(or Austria + neighboring countries if cross-border trips matter), and treat
|
|
"global" as a real future infrastructure decision, not a config value** — see the
|
|
open decision in `docs/project-plan.md` §4.
|
|
|
|
`sync_gtfs.py`'s own filtered SQLite DB (for `/departures`) and OTP's graph (for
|
|
`/plan`) are two **completely separate data pipelines**, on purpose — one small and
|
|
household-specific, one general-purpose and much larger. Nothing here builds or
|
|
manages OTP's graph-build step; that's a one-time (re-run when you update the
|
|
OSM/GTFS inputs) manual operation — see OTP's own docs for the current build command
|
|
for whichever version you deploy.
|
|
|
|
## Voice: "when's the next bus"
|
|
|
|
**Nothing under this repo builds the HA-side custom-sentence/intent-script/
|
|
`rest_command` wiring** — same convention as `identity`'s voice registration and
|
|
every other HA integration point in this project.
|
|
|
|
```yaml
|
|
# configuration.yaml (excerpt) — worked example, unverified against a real instance.
|
|
intent_script:
|
|
NextDeparture:
|
|
speech:
|
|
text: >
|
|
{% if reg_result.content.departures %}
|
|
Next from {{ reg_result.content.stop }}: {{ reg_result.content.departures[0].route }}
|
|
at {{ reg_result.content.departures[0].time }}.
|
|
{% else %}
|
|
I couldn't find a departure for {{ stop | default('your stop') }}.
|
|
{% endif %}
|
|
action:
|
|
- service: rest_command.transit_departures
|
|
data:
|
|
stop: "{{ stop | default('') }}"
|
|
response_variable: reg_result
|
|
|
|
rest_command:
|
|
transit_departures:
|
|
url: "http://127.0.0.1:8099/departures?stop={{ stop | urlencode }}"
|
|
method: GET
|
|
headers:
|
|
Authorization: "Bearer !secret transit_token"
|
|
```
|
|
|
|
Plus a custom sentence (`"when's the next bus"` / `"when's the next bus from
|
|
{stop}"`) mapping to `NextDeparture` — see
|
|
[HA's custom sentences docs](https://www.home-assistant.io/voice_control/custom_sentences/).
|
|
`stop` is optional in the sentence; `TRANSIT_DEFAULT_STOP` covers the bare "when's
|
|
the next bus" case.
|
|
|
|
## Configure
|
|
|
|
```sh
|
|
cp transit/transit.env.example /opt/smart-home/transit/transit.env
|
|
openssl rand -hex 32 # TRANSIT_TOKEN
|
|
chmod 600 /opt/smart-home/transit/transit.env
|
|
$EDITOR /opt/smart-home/transit/transit.env
|
|
```
|
|
|
|
`GTFS_FEED_URL` is the one thing you have to go get by hand — see the template's own
|
|
comment for where. Then run the first sync:
|
|
|
|
```sh
|
|
cd /opt/smart-home && docker compose run --rm transit-sync
|
|
```
|
|
|
|
Check the log for which stops matched `GTFS_STOP_NAMES` — if it says none matched,
|
|
the feed's own `stop_name` spelling differs from what you guessed; `GET /stops`
|
|
(once any sync has run, even a differently-filtered one) lists what's actually in
|
|
the feed near a broader search term.
|
|
|
|
## Manual verification still outstanding
|
|
|
|
1. **The exact `GTFS_FEED_URL` for VVV/VMOBIL** — confirmed the provider and the
|
|
national aggregator exist (`docs/project-plan.md` §1.17's research note), not the
|
|
literal `.zip` download URL, which the household has to get from the aggregator's
|
|
own catalog page.
|
|
2. **GTFS_STOP_NAMES="Kennelbach"** — a reasonable guess at how the feed names the
|
|
local stop(s), not confirmed against the real `stops.txt`.
|
|
3. Post-midnight trips: GTFS allows `departure_time` past `24:00:00` for a service
|
|
day's late-night trips (e.g. `25:30:00` for 1:30 AM the next calendar day).
|
|
`server.py`'s string comparison against the current `HH:MM:SS` handles same-day
|
|
departures correctly but does **not** special-case this — a departure logged as
|
|
`25:30:00` will never match a same-day query after midnight has actually passed.
|
|
Rare for a village bus stop, not fixed here.
|
|
4. Whether VVV's feed includes GTFS-realtime (delays/cancellations) — this only
|
|
reads static schedule data; if a realtime feed exists it isn't consumed, so
|
|
answers are "scheduled," not "actual."
|
|
5. Whether `mobilitaetsdaten.gv.at`'s feed structure matches plain GTFS exactly (all
|
|
the standard files, no unusual encoding) — parsed against the documented GTFS
|
|
spec, not a downloaded copy of this specific feed.
|