saw-livetrack
Shared code for the Live Track Home Assistant integrations.
Separate submodules, deliberately.
saw_livetrack.track — source-agnostic
Holds the last two distinct fixes per object and emits the previous_*
segment fields, so a consumer can animate between two observed positions
rather than extrapolating toward a projection.
The bug it prevents is not specific to any feed. Any integration whose poll interval sits near its source's publish interval receives duplicate fixes, and storing a duplicate as the previous fix collapses the segment to zero length — which renders as a stutter on the map. Every individual update looks correct in isolation; only the sequence is wrong.
The timestamp extractor is injected, because only the source knows what its own timestamp means.
from saw_livetrack.track import FixTracker
t = FixTracker() # or FixTracker(observed_at=my_extractor)
t.update("vehicle-1", record)
attrs = t.segment_fields("vehicle-1") # previous_latitude, course_deg, ...
Keys that cannot be supplied honestly are omitted, never set to None or a
sentinel. A stationary object has no course, and that absence is correct.
saw_livetrack.amtraker — the Amtraker API client
For Live Track Amtrak, Live Track VIA Rail and Live Track Brightline. One cached fetch serves all three providers, since the endpoint takes no parameters and returns every train of every provider.
What it refuses to do, and why — each refusal came from a measurement:
| Behaviour | Reason |
|---|---|
course_deg() always returns None |
the feed's direction is an eight-value octant string; converting it invents precision the feed never carried. A real course comes from two observed positions instead |
speed_kmh() returns None for Brightline even when non-zero |
measured 0.0 on every instance while trains covered 76–133 km at 44–77 mph. The field is unpopulated, not stationary |
observed_at() returns None for Brightline |
its timestamp advances every 30 s while the position changes every 60 s — a feed job clock, not an observation time |
observed_at() returns None for Predeparture trains |
that field then carries a scheduled future departure |
| a finished train is handled by type | the API returns a bare [], not the documented keyed object |
HTTP 429 raises RateLimited |
a non-200 must never be recorded as "no data" |
an empty User-Agent raises at construction |
the server blocks such requests |
amtraker imports from track. Never the reverse.
saw_livetrack.gtfs — GTFS and GTFS-Realtime
Moved here from Live Track Commuter Rail when Live Track Intercity Rail became its second consumer. No Home Assistant code: storage, coordinators, entities and config flows stay in each integration.
| Module | Holds |
|---|---|
realtime |
Obs, one vehicle in one poll; vehicle_time and position_of, the readers every adapter shares; decode |
schedule |
a GTFS static zip distilled to stations, their coordinates, line names, scheduled times for the trips running yesterday, today and tomorrow, and (where the realtime feed names stops only by sequence) stop times; the next station, its estimated arrival, and the delay there (stop_delays: the feed's own delay, else predicted minus scheduled) from TripUpdates; the dataset's feed_version from feed_info.txt when present; and trip_coverage, how many of a TripUpdates feed's trips the cached timetable knows (a low share means it is out of date: Metra's 2026 GTFS static change renames every trip id) |
fleet |
one feed's vehicles across polls: motion, the hold for equipment not in service, held course at rest, grace for a vehicle missing from the feed, and each published record, with delay_min (whole minutes toward zero) and stalled (in service and standing: for REST_S when farther than STALL_M from every stop, for PLATFORM_HOLD_S when at a stop); with an on_rail check, a train in service whose fix is not on rail is held at its last fix on rail for at most REFUSED_HOLD_S, then hidden |
What a given feed actually carries is not here. Each integration keeps one
small adapter per railroad that turns that feed into Obs records and states
only what the feed really populates.
pip install "saw-livetrack[gtfs]"
The gtfs extra brings the protobuf bindings, which only decode needs, and
tzdata, without which schedule cannot read a timetable's times on a system
with no timezone database and publishes no next station.
import saw_livetrack does not import this module, so the Amtraker consumers
neither load it nor need the extra.
saw_livetrack.rail — is this position on a railway?
RailIndex(lines) indexes rail lines, each a sequence of (lat, lon), on a 0.01 degree
grid; distance_m(lat, lon, within_m) and near(lat, lon, within_m) answer from it.
No dependencies. The lines are the caller's: an integration ships them as data
(Live Track Commuter Rail builds one file per railroad from the FRA North American
Rail Network, public domain). Fleet(on_rail=...) takes such a check.
saw_livetrack.zones — the time zone at any coordinate
zone_at(lat, lon) gives the IANA zone name ("America/Chicago"), anywhere on
Earth; open sea gives a nautical zone ("Etc/GMT+5"). local_time(when, lat, lon)
and utc_offset_min(when, lat, lon) give the local clock there for an aware
datetime. Install the zones extra (pip install saw-livetrack[zones]): it brings
tzfpy and tzdata. Without tzfpy, zone_at
raises ImportError. A feed's own stated zone is not trusted: Amtrak's GTFS puts
Arizona on Denver time and the Mississippi Gulf Coast on New York time; the
coordinate puts them right.
saw_livetrack.digitraffic_rail — Finland's trains (Fintraffic Digitraffic)
One poller for every Live Track integration that draws Finnish trains. It joins
train-locations/latest (every train with a fix, passenger or not) to the train
list (trains/<date>, then trains?version= for only what changed), and routes
each train to ONE consumer by trainCategory: Long-distance to intercity,
Commuter to commuter, every other category (cargo, locomotives, shunting,
track machines, test runs) to freight. One train TYPE overrides its category:
MV, empty passenger stock, which Digitraffic files under Long-distance,
goes to freight (it carries no passengers), and so does V, a depot or yard
train (varikkojuna). next_stop(train) gives the next
station, its ETA and the delay in whole minutes. Sends the Accept-Encoding: gzip
Digitraffic requires and a Digitraffic-User header. No extra dependencies.
Share one client per Home Assistant with reference/shared_digitraffic_rail.py:
each integration registers the consumer it draws, and the client keeps
timetables only for registered consumers, so an integration installed alone
costs only its own trains (0.2.7). Every fetch and table change is under one
lock; register() and unregister() never wait on it. Each joined record
carries its train.
Data attribution
Train data is provided by Amtraker and is licensed under the Open Data Commons Attribution License (ODC-By) v1.0.
Time-zone boundaries (saw_livetrack.zones) are from
Timezone Boundary Builder,
embedded in tzfpy, and are licensed under the
Open Database License (ODbL) v1.0.
zones.DATA_VERSION() names the release.
Finnish rail data (saw_livetrack.digitraffic_rail) is from Fintraffic /
digitraffic.fi, licensed under
Creative Commons Attribution 4.0.
Disclaimer
Independent project. Not affiliated with, endorsed by or connected to Amtrak, VIA Rail Canada, Brightline, VR or Fintraffic.
Tests
python tests/test_track.py
python tests/test_amtraker.py
python tests/test_shared_coordinator.py
python tests/test_digitraffic_rail.py
python -m unittest discover -s tests -p "test_gtfs_*.py"
python -m unittest tests.test_zones
The first four are scripts that exit non-zero on a failed check; run them
directly, not through unittest discover.
Licence
MIT.
Metadata
Release files for saw-livetrack 0.2.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| saw_livetrack-0.2.7.tar.gz | 70.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| saw_livetrack-0.2.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 113.8 kB
Release files / saw_livetrack-0.2.7.tar.gz
| Download URL | saw_livetrack-0.2.7.tar.gz |
|---|---|
| Size | 70.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2aa9779ea888b79cdd23e90b9d614c6e807fe469c3ffb87a128ea39aad0a7fb1
|
|
BLAKE2b-256 checksum How to use checksums |
adfca4311b88976b751aaf6e010a706e138e7196dde7a83ff2f9c64228fde99d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|
Release files / saw_livetrack-0.2.7-py3-none-any.whl
| Download URL | saw_livetrack-0.2.7-py3-none-any.whl |
|---|---|
| Size | 42.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
72fabccf140ef7768b5562845988d17a220503ee455b6c8ff41b19594ffb20ee
|
|
BLAKE2b-256 checksum How to use checksums |
3be3726ed8fc723d298aa16e36c76f633f9538665d9dc1c956cdb564e5ad240b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.13
|