SmartestHome/transit/README.md

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.