Phase 19: trash-calendar (Kennelbach ICS -> CalDAV) and transit (GTFS
departures + OpenTripPlanner-backed voice route planning, slow walking
speed assumed). Phase 20: Tapo camera integration via identity's new
Frigate face-recognition presence signal, and chores, a presence/
calendar-driven household task nudger ("I don't care who does it, as
long as it gets done") with a passive fairness tally, litter culprit
attribution, and per-person chore_exempt/chore_reminder_style settings
on identity.
Also: the HA integrations + hardware-monitoring catalogs, Music
Assistant wiring, docs/network-integration.md (OPNsense VLAN
segmentation, no WAN port-forwards), the ESPHome BLE-proxy firmware,
and RuView CSI-presence integration (github.com/ruvnet/ruview) with
household automations for sleep/distress/concurrent-vitals/bathroom-
occupancy — all flagged with their real unverified assumptions and
open decisions. Fixed two real port collisions surfaced while wiring
this in (OTP vs. zigbee2mqtt on 8080, Music Assistant vs. pantry-vision
on 8095).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CrKjW3yVptUnjG35HjC1
|
||
|---|---|---|
| .. | ||
| Dockerfile | ||
| README.md | ||
| server.py | ||
| sync_gtfs.py | ||
| transit.env.example | ||
README.md
transit
"When's the next bus?" via voice, from Phase 19 of the project plan.
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_TRANSITinsetup-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 fromcalendar.txt/calendar_dates.txt(which services run today) joined againststop_times.txtfor the matched stop(s). Published, likeidentity/pantry-vision—homeassistantrunsnetwork_mode: hostin this stack and can't resolve plain container DNS names, so arest_commandneeds a real published port to reach this on, same reasoning asidentity/identity.env.example's note aboutHA_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 (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) 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.
# 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.
stop is optional in the sentence; TRANSIT_DEFAULT_STOP covers the bare "when's
the next bus" case.
Configure
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:
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
- The exact
GTFS_FEED_URLfor VVV/VMOBIL — confirmed the provider and the national aggregator exist (docs/project-plan.md§1.17's research note), not the literal.zipdownload URL, which the household has to get from the aggregator's own catalog page. - GTFS_STOP_NAMES="Kennelbach" — a reasonable guess at how the feed names the
local stop(s), not confirmed against the real
stops.txt. - Post-midnight trips: GTFS allows
departure_timepast24:00:00for a service day's late-night trips (e.g.25:30:00for 1:30 AM the next calendar day).server.py's string comparison against the currentHH:MM:SShandles same-day departures correctly but does not special-case this — a departure logged as25:30:00will never match a same-day query after midnight has actually passed. Rare for a village bus stop, not fixed here. - 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."
- 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.