Shared code for the Live Track Home Assistant integrations.
Separate submodules, deliberately.
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.
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.
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.
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.
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.
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, looking only after the last
row with an actual time: Digitraffic can leave a stop the train made without
one (0.2.25). 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.
One poller for every Live Track integration that draws Renfe's vehicles. Renfe
publishes ONE GTFS-Realtime feed of positions and one of trip updates for all its
Cercanías networks and Rodalies de Catalunya, keyless, about every 20 s, and the
feed carries more than commuter trains: rail-replacement and feeder buses
(route_type 3 in Renfe's own timetable) and Catalonia's regional lines (R11-R17).
Each vehicle goes to ONE consumer: bus for a bus, intercity for a regional
line, commuter for every other train. for_consumer(name) returns that
consumer's positions and trip updates as two GTFS-Realtime feeds, cleaned once for
everyone: the trip id is the identity (Renfe's entity ids repeat across networks),
a trip appearing twice is kept once, 0,0 is dropped, and a position that jumps
straight back to where the train was two fixes earlier is held. The timetable
(16 MB, for telling the buses from the trains) loads on its own thread and then
daily; until the first load ends every feed is empty, so a bus is never drawn as
a train, and static_zip() hands the same bytes to a consumer that distills them.
parse_trip_id() reads the network, train number and line out of a trip id. Needs
the gtfs extra. Share one client per Home Assistant with
reference/shared_renfe.py; each integration registers the consumer it draws
(0.2.9). Renfe leaves a whole network out of about one batch in four; such a
network is served from the batch it was last in for up to a minute, positions
and trip updates both (0.2.10).
One poller for every Live Track integration that draws TfNSW's regional vehicles,
each user with their own key (Authorization: apikey <key>; each config entry
passes its own, and the client polls with the oldest). Each vehicle goes to ONE
consumer: intercity for route type 106, commuter for 100 (4T.T.BMT, HUN,
SCO, SHL), bus for 204 and 205 (4T.C.), by the route_id prefix until a
timetable is on disk and by its route_type then. for_consumer(name) returns that
consumer's positions and trip updates as two GTFS-Realtime feeds, cleaned once for
everyone: a trip with a position keeps only that run's update, since one trip id
runs on consecutive dates and its updates overlap; the position's stop_id,
current_stop_sequence, current_status and speed are cleared, since they mislead.
The timetable (about 40 MB, no Range, no ETag) goes to
.storage/saw_tfnsw_nswtrains.zip on its own thread, checked by HEAD at most daily;
static_version() and static_path() let each consumer distill its own cut. A key is
given up on after 10 minutes of HTTP 401 or 403 from both feeds with no success between,
then AuthRefused when none is left: one or two 401s are normal here. Share one
client per Home Assistant with reference/shared_tfnsw.py (0.2.23). Needs the gtfs extra.
One poller for every Live Track integration that draws Norwegian trains, keyless (Entur asks
each consumer for an ET-Client-Name, sent as one fixed name). Polled per CODESPACE, the
railroad (VYG Vy, GOA Go-Ahead Norge, FLT Flytoget; SJ Nord publishes no positions),
and only the codespaces some consumer registered: register(consumer, codespace). Every
train is route type 100, so each vehicle goes to ONE consumer by its line's letter:
intercity for F, RE, RX and R lines, commuter for L lines and all of Flytoget, bus
for rail-replacement buses (trip ids BUS-<n> and B<n>, which ride on rail lines). The
line is the route's short name from the timetable, or a route id that carries it
(VYG:Line:R13); anything else goes to nobody until the timetable is on disk.
for_consumer(consumer, codespace) returns that consumer's positions and trip updates,
cleaned once for everyone: start_date rewritten to YYYYMMDD (the trip updates often
send YYYY-MM-DD), and a trip with a position keeps only that run's updates. Each
codespace's timetable (Vy's is 66 MB) goes to .storage/saw_entur_<codespace>.zip on its
own thread, checked by HEAD at most daily; static_version(codespace),
static_path(codespace) and routes(codespace) let each consumer distill its own cut.
Share one client per Home Assistant with reference/shared_entur.py (0.2.27). Needs the
gtfs extra.
One poller for every Live Track integration that draws Swedish trains, with each user's own free key
(register(consumer, brand, key); the oldest key is used, a refused one given up on after 10 minutes, as the
other keyed clients). Each poll reads the positions as changeid deltas, the announcements of each run newly
seen whole (a positioned train's run is its advertised number and run date), and the rows of the claimed runs
modified since the last answer: a day's announcements (about 69,000 rows) are never read whole. Each run goes to
ONE consumer and brand by its traffic type and product: Tåg to intercity under its brand (sj,
oresundstag, ...), Pendeltåg and Arlanda Express to commuter, Buss to transit; Vy's own trains to
nobody (Norway's). for_consumer(consumer, brand) returns that brand's positioned runs with the calls of the trip
each train is on; stations() the stations, read daily to .storage/saw_trafikverket_stations.json. Share one
client per Home Assistant with reference/shared_trafikverket.py (0.2.31). No extra needed.
Irish Rail's keyless realtime API (getHaconTrainsXML, undocumented; the documented getCurrentTrainsXML when it
fails), read once a minute for every consumer and split by the train code's prefix: A and B to intercity, P and D to
commuter, E (DART) and BUS to transit. Positions of 0,0 are no position; times are Irish local time. Also the
stations (weekly, .storage/saw_irish_rail_stations.json) and the National Transport Authority's Irish Rail GTFS
(daily HEAD, .storage/saw_nta_gtfs_irish_rail.zip), each kept on disk. Share one client per Home Assistant with
reference/shared_irish_rail.py (0.2.32). No extra needed (gtfs brings tzdata where the OS has none; without it
Ireland's own summer-time rule is used).
Since 0.2.34 the timetable carries every train and Irish Rail's reports correct it (its positions are signalling
points, minutes apart, and some lines are not tracked at all): read_timetable(zip, consumer, today) gives a
consumer's trips, calls and service dates from the NTA's zip; join_stations matches the API's stations to the
timetable's stops; RunTracker().step(trains, now, table, stations) returns each run drawn this poll, keyed
<code>-<day>, with its last reported delay and where it is (standing at, or running from, a call). A report counts
only when it is that run's (origin time, not a ghost, not an hour early); its last location is a floor. Direction,
look and what is published stay with each consumer.
For a railroad whose positions are scarce or absent (0.2.34). call_times(calls) gives each call one arrival and one
departure: its actual, else expected, else aimed time plus the delay last known before it, refusing an arrival
faster than 0.3 of the scheduled stretch; place(times, now) says whether the train is standing at a call or
running from it toward the next, or not out; service_base(day, tz) is the GTFS service day's zero. No
dependencies.
NS's Virtual Train positions (/virtual-train-api/vehicle, each user's own key from apiportal.ns.nl, header
Ocp-Apim-Subscription-Key), read once for every consumer and routed by the feed's type: IC and the regional ARR
trains to intercity, SPR to commuter. The feed has no fix time: observed_at is the poll that first saw a
CHANGED position, flagged observed_at_source: "first_seen", never re-stamped while it stands. The track is NS's
SpoorKaart (station-to-station lines, daily, .storage/saw_ns_spoorkaart.json); the timetable OVapi's
gtfs-nl.zip (weekly, conditional, cut to rail as it is read, .storage/saw_ovapi_nl_rail.json.gz), which names
each train's trip by its number; delays OVapi's trainUpdates.pb every five minutes, conditional, backing off on
429 (one server for both). A regional train's company (arriva, keolis) is NS's DVS vervoerder, else its
brand's route. for_consumer(name, operator) gives each train with its trip, calls (expected times from the
updates) and path (SpoorKaart stations); pair_lines() the encoded lines; next_call(calls, now) the next stop
and delay. Share one client per Home Assistant with reference/shared_ns.py (0.2.35). The updates need the gtfs
extra.
The GTFS Schedule is one zip of numbered folders, and a consumer takes only its own
(intercity folder 1, bus folder 5) by HTTP Range. The address is read from the
portal's CKAN API each time, because it changes. Checked daily, fetched when the ETag
changes, the ETag held on every response and the CRC at the end; a 200 to a Range
request is abandoned unread. static_path(consumer) is the folder as a zip at
.storage/saw_transport_vic_<folder>.zip; no key needed. Realtime, V/Line's only so far:
for_consumer("intercity"), as published, with the user's own key (KeyId). No
V/Line rule lives here. Share one client per Home Assistant with
reference/shared_transport_vic.py (0.2.23). Realtime needs the gtfs extra.
Every Live Track integration ships the released
Live Track Map Card,
serves it and offers it; the instance loads the highest version, from exactly
one dashboard resource row. This module decides: it reads the version stamped
on the bundle's first line, orders versions by SemVer precedence (build
metadata ignored, ties keep what is registered), builds and parses the
resource URL (?v= percent-encoded), and plans the change to the row. It never
touches a row under /hacsfiles/, and it updates rows in place rather than
adding them. No dependencies, no Home Assistant import. Each integration
copies reference/shared_map_card.py verbatim for the Home Assistant calls
(0.2.8).
The marker icons of the Live Track aircraft integrations, so each draws an
aircraft the same way: the ICAO type designator to icon table, the SVG
artwork, render() (fill by owner class, a dark edge, gentle shading,
police lights, a size ramp by ADS-B emitter category), and ART_REV, the
picture revision a consumer puts in its URLs. Each integration serves the
pictures from its own view; picture_query() gives the query string. No
dependencies, no Home Assistant import (0.2.11). Since 0.2.12 the lights take
a mode: True pairs them with the fill (the blue wig-wag on the police white,
the red/blue glow on any other), "glow" and "wigwag" say which, and the
query carries it as l=g or l=w; lights_from_query() reads it back, the
old l=1 included. Since 0.2.16 "charter" (LIGHTS_CHARTER, l=c) draws a
steady glow, blue inside and red outside, for an ICE Air charter airframe.
Reads the public EnforcementAircraft
list (release data) once per Home Assistant instance: shared(hass.data, cache_path) gives the one reader, refresh(session) checks SHA256SUMS
first and fetches the list only when it changed, installing it only when its
bytes match. A failure keeps the cached copy (.storage/saw_enforcementaircraft.json),
which never expires. Lookups by Mode S hex: status(), is_sourced(),
hexes(*statuses). Statuses: SOURCED, DETECTED, CHARTER (ice_air_charter,
0.2.16: an airliner that flies ICE Air charters and other work too) and
CLEARED; WATCHED is every one but cleared, and CHARTER_LABEL ("ICE Air Charter?") is the
label every consumer prints for a charter. ice_air_callsign(callsign, hex) (0.2.18) says
whether THIS flight broadcasts ICE Air's call sign: "VELUX", "GXAUX" or "UX" (each + 2 digits, the last two 0.2.22), "TYSON"
(TYSON + 2 digits) or "TYS" (TYS + 1-4 digits), on a US Mode S address only (A00000-AFFFFF) and
never for AA3410, else None; a carrier's own designator (GXA, BBQ) says nothing either way. While
it does, a consumer prints CHARTER_FLIGHT_LABEL ("ICE Air Charter"), without the question
mark. Per flight only: it says nothing about the airframe's next flight. A status the reader does not
know is installed as given, so a newer list never breaks an older reader. The consumer passes its own aiohttp session, made with
SESSION_KWARGS (GitHub's release redirect carries a header over aiohttp's
default 8,190-byte limit). No Home Assistant import, no aiohttp import (0.2.12).
Most of the icons are by ADS-B Radar, from its
SVG aircraft icon set, used under
the author's terms, which ask for a backlink to ADS-B Radar. They are not
covered by this package's MIT licence, which covers the code; the icons drawn
for this project were donated to ADS-B Radar and are under the same terms.
Which is which, and what was changed:
src/saw_livetrack/aircraft_icons/ATTRIBUTION.md.
The aircraft integrations ask adsb.fi through one poller per Home Assistant
instance (shared(hass.data)), because a free feed's rate limit is counted per
IP address, not per integration. Each consumer names the Mode S hexes it wants
(set_hexes) and one request a minute asks for all of them (hex_round); a
circle (circle) asked again within a few seconds by anyone is answered from
the same request; a consumer's circle lying wholly inside another consumer's
recent answer is answered from that answer, filtered by distance, with no
request at all (0.2.14); every request to one source is spaced a second apart; and
one HTTP 429 backoff covers every consumer. Each consumer says which aircraft
it draws (claim) so another can leave them alone (claimed_by_others). The
circle's sources are the caller's choice; the hex request goes to adsb.fi, and
to adsb.lol only while adsb.fi is failing (0.2.24). A source that keeps failing
in any other way (a timeout, a broken connection, another status) gets a skip
window like a 429's, so one hung feed does not stall every consumer, and the log
carries one WARNING when a source starts failing and one when it has answered
again. No Home Assistant import, no aiohttp import (0.2.13).
saw_livetrack.ais is the shape every AIS source yields (Observation, units in the field names, None
for anything a transmitter left unavailable), the rate-of-turn decode (rot_to_dps), what a transmitter
is (object_kind: vessel, net buoy, aid to navigation, base station, SAR aircraft, distress beacon) and the
MMSI join table for static data (StaticCache). Moved from Live Track Ships in 0.2.19.
saw_livetrack.aisstream reads aisstream.io through one client per Home
Assistant instance (shared(hass.data)). aisstream gives each connection one subscription, and an MMSI
filter applies to every box in it, so the client holds at most two connections per API key: an AREA
subscription, the union of every area consumer's boxes (add_area), and a VESSELS subscription, the
union of every list consumer's MMSIs, at most 200 (add_vessels). Each observation goes to the consumers
whose boxes hold it (and whose list names it), labelled with that consumer's domain. A change of
consumers replaces the subscription on the open connection, at most once a second; the last consumer of
a connection closes it. Static messages feed one StaticCache (client.statics). Each user brings their
own aisstream API key. No Home Assistant import; aiohttp only when it connects (0.2.19).
Each consumer says which vessels it draws (claim) so another can leave them alone (claimed_by_others;
release, and remove drops a consumer's claims), as on adsb_poll. A VESSELS consumer with no valid MMSI
is refused, since aisstream would read an empty MMSI filter as none at all (0.2.20).
Reads the public EnforcementVessels
list (release data) once per Home Assistant instance: shared_list(hass.data, cache_path) gives the one
reader, refresh(session) checks SHA256SUMS first and fetches the list only when it changed, installing
it only when its bytes match. A failure keeps the cached copy (.storage/saw_enforcementvessels.json),
which never expires. Lookups by MMSI: vessel() (status, name, agency, the entry's short form, hull) and
watched() (every MMSI not cleared: what a VESSELS subscription asks for); theatre(lat, lon) gives the
box where the list says the work happens, if any. A status the reader does not know is installed as given.
No Home Assistant import, no aiohttp import (0.2.20; moved from Live Track SALT).
The look of a listed vessel, the same for every integration that draws one (0.2.21; moved from Live Track
Ships): listed(reader, mmsi) (the row, unless cleared), label(v) (the entry's short form, else the agency,
else "Listed vessel", at most 22 characters), name(v, ais_name) and operator_short(v) for the hover's first
line, attributes(v, ais_name) (unknown values omitted), lights(v) (the glow on a sourced row), LIST_TONE
("mil", the Coast Guard's army green in saw_livetrack.aircraft_icons) and attribution(base), the
source's credit followed by the list's ODbL line (LIST_ATTRIBUTION). The picture stays the integration's.
The FAA's Releasable Aircraft Database,
read once per Home Assistant instance for the aircraft integrations (0.2.15): shared(hass.data, storage_dir) gives the one registry, load() reads the caches (.storage/saw_faa_gov_aircraft.json,
saw_faa_owners.sqlite), and refresh(session) downloads the archive when the cache is a week old or
of an older rule, under one lock, so two integrations never download it twice. A failure keeps the
tables in use; the owner table never breaks the government one. lookup_government(hex) gives a
government registrant's registration, owner, owner class (categorise: federal law enforcement,
law enforcement, Coast Guard, military, fire and EMS, ...) and, for law enforcement, a short name
(operator_short: DHS, CBP, ICE, FBI, USMS, DEA, ATF, USSS, USCG, DOJ, or a state or local force
shortened to 22 characters). lookup_owner(hex) gives any registration's title holder.
from_feed_flags() reads the feed's military and PIA flags. The data comes from the FAA, which
withholds what 49 U.S.C. 44114 requires before it publishes; nothing is shipped. No Home Assistant
import, no aiohttp import.
lookup_operator(callsign) gives (designator, operator name) for an airline callsign (UAL308 -> ("UAL", "United Airlines")), (None, "Private") for a US registration. airline_short(callsign) gives the name a person reads on the map's hover, "United" in "United 1678", for a known designator followed by a flight number (7 characters at most): a single-brand regional by its brand (ENY -> American Eagle), a multi-brand one by its operator (SkyWest, Republic). None for a special callsign (TROY329, VELUX55) or a registration. A hand-kept table of about 160 designators; moved here from Live Track Aircraft in 0.2.17 so Live Track SALT reads the same one.
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.
Spanish rail data (saw_livetrack.renfe) is from Renfe,
licensed under
Creative Commons Attribution 4.0.
Transport for NSW data (saw_livetrack.tfnsw) is from Transport for NSW,
and Victorian data (saw_livetrack.transport_vic) from Transport Victoria,
both licensed under
Creative Commons Attribution 4.0. Each user brings their own key;
nothing here redistributes the data.
Norwegian rail data (saw_livetrack.entur): contains data under the
Norwegian licence for Open Government data (NLOD)
distributed by Entur. Nothing here redistributes the data.
Swedish rail data (saw_livetrack.trafikverket) is from Trafikverket,
released under CC0 1.0. Each user brings their own key;
nothing here redistributes the data.
The enforcement-aircraft list (saw_livetrack.enforcement_list) contains
information from EnforcementAircraft,
which is made available under the
Open Database License (ODbL) v1.0.
The enforcement-vessels list (saw_livetrack.enforcement_vessels) contains
information from EnforcementVessels,
which is made available under the
Open Database License (ODbL) v1.0.
Aircraft positions (saw_livetrack.adsb_poll) are from adsb.fi,
and from adsb.lol (ODbL v1.0) where a consumer asks it and for the hex
request while adsb.fi is failing; each consumer credits the feeds it uses.
Vessel positions (saw_livetrack.aisstream) are from aisstream.io, which
publishes no terms of use; each consumer credits it.
Irish rail data (saw_livetrack.irish_rail) is from Irish Rail (irishrail.ie), which publishes its
realtime API "as is" with no licence stated, and the timetable from the National Transport Authority, CC BY 4.0, provided as is.
Dutch train positions and track (saw_livetrack.ns) are from NS (apiportal.ns.nl),
read with each user's own key; the timetable and delays from OVapi, CC0.
Independent project. Not affiliated with, endorsed by or connected to Amtrak, VIA Rail Canada, Brightline, VR, Fintraffic, Renfe, Transport for NSW, Transport Victoria, Entur, Trafikverket, Irish Rail (Iarnród Éireann), the National Transport Authority, NS, Arriva, Keolis or OVapi.
python tests/test_track.py
python tests/test_amtraker.py
python tests/test_shared_coordinator.py
python tests/test_digitraffic_rail.py
python tests/test_map_card.py
python tests/test_shared_map_card.py
python tests/test_renfe.py
python tests/test_tfnsw.py
python tests/test_transport_vic.py
python tests/test_aircraft_icons.py
python -m unittest discover -s tests -p "test_gtfs_*.py"
python -m unittest discover -s tests -p "test_enforcement_list.py"
python -m unittest tests.test_adsb_poll
python -m unittest tests.test_aisstream
python -m unittest tests.test_enforcement_vessels
python -m unittest tests.test_zones
python -m unittest tests.test_entur tests.test_shared_entur
python -m unittest tests.test_trafikverket tests.test_shared_trafikverket
python -m unittest tests.test_irish_rail tests.test_shared_irish_rail tests.test_irish_rail_runs
python -m unittest tests.test_timetable
python tests/test_ns.py
The first four, test_aircraft_icons.py and test_ns.py are scripts that exit non-zero on a failed check; run them
directly, not through unittest discover.
MIT, except the ADS-B Radar aircraft icons, which are under their author's
terms: see src/saw_livetrack/aircraft_icons/ATTRIBUTION.md.