onboardapis
onboardapis
Description
onboardapis allows you to interact with different on-board APIs. You can connect to the Wi-Fi of a supported transportation provider and access information about your journey, the vehicle you are traveling in and much more.
Installation
Install the latest stable version of onboardapis from PyPI using pip:
$ python -m pip install onboardapis
Install the latest development version of onboardapis from GitHub using pip:
$ python -m pip install git+https://github.com/felix-zenk/onboardapis.git
Quickstart
To begin with development, you will need to know a few things first:
- What vehicle type do you want to use?
- Who operates the vehicle?
- What country is the operator from?
With this information you can get the necessary module from the package
onboardapis.<type>.<country>.<operator> and import the API class from it.
For more specific information on finding the API you are looking for,
see Finding your API.
Example: Let's say you want to use the on-board API called ICE Portal of Deutsche Bahn trains in Germany:
from onboardapis.train.de.db import ICEPortal
Every vehicle has an init-method that needs to be called to initialize the connection to the API.
When using a vehicle as a context manager the init-method will automatically be called.
from onboardapis.train.de.db import ICEPortal
from onboardapis.units import kilometers, kilometers_per_hour
# init is automatically called
with ICEPortal() as train:
print(
f"Distance to {train.current_station.name}:",
f"{kilometers(meters=train.calculate_distance(train.current_station)):.1f} km"
)
# init has to be explicitly called
train = ICEPortal()
train.init() # Explicit call to init method to initialize API connection
print(
f"Travelling at {train.speed} m/s",
f"({kilometers_per_hour(meters_per_second=train.speed):.2f} km/h)",
f"with a delay of {train.delay.total_seconds():.0f} seconds"
)
And there you go!
You can read more information about available attributes in the onboardapis.train.Train and onboardapis.mixins documentation
and the respective train's documentation.
Note: As you may have noticed by now, the package always returns
datetimeortimedeltaobjects for time-based values and other values like distances, velocity, etc. in SI units, so you have to convert to other units if you want to use values different from the SI units. For convenience theonboardapis.unitsmodule provides functions to convert between units.The name of a conversion function is the unit that will be the result of the conversion. Different units can be passed to a conversion function as keywords. Keywords can be combined to return the sum of the input units.
Documentation
Access the documentation on GitHub-Pages.
Supported APIs
| API | API features | Type | Country | Operator |
|---|---|---|---|---|
| RailnetRegio | basic, geo | train | at (Austria) | obb (Österreichische Bundesbahnen) |
| ICEPortal | online, vehicle, geo, journey | train | de (Germany) | db (Deutsche Bahn / DB AG) |
| FlixTainment | basic, geo | train | de (Germany) | flix (Flix Train GmbH) |
| MetronomCaptivePortal | online | train | de (Germany) | me (metronom Eisenbahngesellschaft mbH) |
| FlyStream | basic, geo, basic-journey | plane | de (Germany) | cfg (Condor Flugdienst GmbH) |
Experimental APIs
| API | API features | Type | Country | Operator |
|---|---|---|---|---|
| PortalINOUI | basic, vehicle, geo, journey | train | fr (France) | sncf (SNCF Voyageurs) |
| RegioGuide / ZugPortal | basic, vehicle, geo, journey | train | de (Germany) | db (Deutsche Bahn / DB AG) |
| PortaleRegionale | basic, basic-journey | train | it (Italy) | ti (Trenitalia S.p.A.) |
| SBahnHannover | online*, basic-journey | train | de (Germany) | tdh (Transdev Hannover GmbH) |
| České dráhy | basic, geo | train | cz (Czech Republic) | cd (České dráhy s.a.) |
* Not supported yet.
APIs in development
| API | API features | Type | Country | Operator |
|---|---|---|---|---|
| UnnamedWideroePortal | basic, geo, basic-journey | plane | no (Norway) | wif (Widerøe's Flyveselskap) |
| UnnamedMarabuPortal | basic, geo, basic-journey | plane | ee (Estonia) | mbu (Marabu Airlines OÜ) |
| UnnamedSmartwingsPortal | basic, geo, basic-journey | plane | cz (Czech Republic) | tvs (Smartwings a.s) |
| ... |
Finding your API
1. Determine the vehicle type: train, plane, bus, ship, other.
2. Look up the ISO 3166-2 country code of the operators' country
3. Operator code
The operator code is vehicle-type-specific. The following IDs are used:
| Vehicle type | Region | Register |
|---|---|---|
| plane | global | ICAO |
| train | europe | VKM |
Combine these three values to onboardapis.<type>.<country>.<operator>.
This is the module that contains the API.
Hint: You can also get the module path by looking at Supported APIs / Experimental APIs and taking the three values from there.
API features
The API features define what information can be accessed through the API
and are a general indicator of the API's capabilities.
Features can be combined.
The current possible API features are:
basic: Basic information is available such as connection status to the API.online: The API supplies the user with internet access, and the internet access can be enabled and disabled.vehicle: The API supplies information about the vehicle such as the ID, line number, etc.geo: The API supplies information about the current location, speed, etc. of the vehicle.basic-journey: The API supplies basic journey information including the current station and the destination station.journey: The API supplies detailed journey information including all the stations and possibly connecting services.
1""" 2.. include:: ../README.md 3 4--- 5""" 6from __future__ import annotations 7 8import logging 9 10from abc import ABCMeta 11from dataclasses import dataclass 12from datetime import datetime 13from typing import Iterable 14 15from .data import ID, API, ThreadedAPI, ScheduledEvent, Position 16from .exceptions import InitialConnectionError 17 18logger = logging.getLogger(__name__) 19 20__all__ = [ 21 "bus", 22 "data", 23 "exceptions", 24 "mixins", 25 "other", 26 "plane", 27 "protocols", 28 "ship", 29 "train", 30 "units", 31 "Vehicle", 32 "Station", 33 "ConnectingVehicle", 34] 35 36 37class Vehicle(metaclass=ABCMeta): 38 """ 39 Base class for all vehicles 40 """ 41 42 _api: API 43 """The :class:`API` that supplies the data for this vehicle.""" 44 45 def __enter__(self): 46 self.init() 47 return self 48 49 def __exit__(self, exc_type, exc_val, exc_tb): 50 self.shutdown() 51 52 def init(self) -> None: 53 """Initialize the connection to the API. 54 55 Call the init method of the API that supplies the data for this vehicle. 56 57 Raises: 58 InitialConnectionError: If the connection to the API could not be established. 59 """ 60 if not hasattr(self, '_api'): 61 return # Abstract class without API implementation 62 63 try: 64 self._api.init() 65 if isinstance(self._api, ThreadedAPI): 66 self._api.start() 67 # noinspection PyProtectedMember 68 if not self._api.ready.wait(timeout=max(self._api._interval * 1.5, 15)): 69 raise RuntimeError('API readiness period timed out!') 70 return 71 except RuntimeError as e: 72 raise InitialConnectionError from e 73 74 def shutdown(self) -> None: 75 """ 76 This method will be called when exiting the context manager. 77 78 :return: Nothing 79 :rtype: None 80 """ 81 if not hasattr(self, '_api'): 82 return # Abstract class without API implementation 83 84 if isinstance(self._api, ThreadedAPI): 85 self._api.stop() 86 87 @property 88 def now(self) -> datetime: 89 """ 90 Get the current time as seen by the vehicle 91 92 :return: The current time 93 :rtype: datetime.datetime 94 """ 95 return datetime.now() 96 97 @property 98 def id(self) -> ID: 99 """ 100 :return: The ID of the vehicle 101 :raises DataInvalidError: If the ID could not be fetched from the server 102 """ 103 return 'undefined' 104 105 106@dataclass 107class ConnectingVehicle(object): 108 """ 109 A connecting vehicle is a vehicle that is not part of the main trip but of a connecting service. 110 It may only have limited information available. 111 """ 112 vehicle_type: str | None 113 """The abbreviated vehicle type""" 114 line_number: str | None 115 """The line number of the vehicle""" 116 departure: ScheduledEvent[datetime] | None 117 """The departure time of the vehicle""" 118 destination: str | None 119 """The destination of the vehicle""" 120 121 122@dataclass 123class Station(object): 124 """ 125 A Station is a stop on the trip 126 """ 127 # noinspection PyTypeHints 128 id: ID 129 """The ID of the station""" 130 name: str 131 """The name of the station""" 132 arrival: ScheduledEvent[datetime] | None 133 """The arrival time at this station""" 134 departure: ScheduledEvent[datetime] | None 135 """The departure time from this station""" 136 position: Position | None 137 """The geographic position of the station""" 138 distance: float | None 139 """The distance from the start to this station""" 140 _connections: Iterable[ConnectingVehicle] 141 142 def __str__(self) -> str: 143 return self.name 144 145 @property 146 def connections(self) -> list[ConnectingVehicle]: 147 """The connecting services departing from this station.""" 148 if not isinstance(self._connections, list): 149 self._connections = list(self._connections) 150 return self._connections 151 152 def calculate_distance(self, other: Station | Position | int | float) -> float | None: 153 """ 154 Calculate the distance in meters between this station and something else. 155 156 Accepts a :class:`Station`, :class:`Position` or a number for the distance calculation. 157 158 :param other: The other station or position to calculate the distance to 159 :type other: Station | Position | int | float 160 :return: The distance in meters 161 :rtype: Optional[float] 162 """ 163 # If there is not enough information to calculate the distance, return None 164 if other is None: 165 return None 166 167 # Both distances since the start are known 168 if isinstance(other, (int, float)) and self.distance is not None: 169 return abs(self.distance - other) 170 171 # Both positions are known 172 if isinstance(other, Position) and self.position is not None: 173 return self.position.calculate_distance(other) 174 175 # Both are a station 176 if isinstance(other, Station): 177 if self.distance is not None and other.distance is not None: 178 return abs(self.distance - other.distance) 179 if self.position is not None and other.position is not None: 180 return self.position.calculate_distance(other.position) 181 # No distance could be determined 182 return None
38class Vehicle(metaclass=ABCMeta): 39 """ 40 Base class for all vehicles 41 """ 42 43 _api: API 44 """The :class:`API` that supplies the data for this vehicle.""" 45 46 def __enter__(self): 47 self.init() 48 return self 49 50 def __exit__(self, exc_type, exc_val, exc_tb): 51 self.shutdown() 52 53 def init(self) -> None: 54 """Initialize the connection to the API. 55 56 Call the init method of the API that supplies the data for this vehicle. 57 58 Raises: 59 InitialConnectionError: If the connection to the API could not be established. 60 """ 61 if not hasattr(self, '_api'): 62 return # Abstract class without API implementation 63 64 try: 65 self._api.init() 66 if isinstance(self._api, ThreadedAPI): 67 self._api.start() 68 # noinspection PyProtectedMember 69 if not self._api.ready.wait(timeout=max(self._api._interval * 1.5, 15)): 70 raise RuntimeError('API readiness period timed out!') 71 return 72 except RuntimeError as e: 73 raise InitialConnectionError from e 74 75 def shutdown(self) -> None: 76 """ 77 This method will be called when exiting the context manager. 78 79 :return: Nothing 80 :rtype: None 81 """ 82 if not hasattr(self, '_api'): 83 return # Abstract class without API implementation 84 85 if isinstance(self._api, ThreadedAPI): 86 self._api.stop() 87 88 @property 89 def now(self) -> datetime: 90 """ 91 Get the current time as seen by the vehicle 92 93 :return: The current time 94 :rtype: datetime.datetime 95 """ 96 return datetime.now() 97 98 @property 99 def id(self) -> ID: 100 """ 101 :return: The ID of the vehicle 102 :raises DataInvalidError: If the ID could not be fetched from the server 103 """ 104 return 'undefined'
Base class for all vehicles
53 def init(self) -> None: 54 """Initialize the connection to the API. 55 56 Call the init method of the API that supplies the data for this vehicle. 57 58 Raises: 59 InitialConnectionError: If the connection to the API could not be established. 60 """ 61 if not hasattr(self, '_api'): 62 return # Abstract class without API implementation 63 64 try: 65 self._api.init() 66 if isinstance(self._api, ThreadedAPI): 67 self._api.start() 68 # noinspection PyProtectedMember 69 if not self._api.ready.wait(timeout=max(self._api._interval * 1.5, 15)): 70 raise RuntimeError('API readiness period timed out!') 71 return 72 except RuntimeError as e: 73 raise InitialConnectionError from e
Initialize the connection to the API.
Call the init method of the API that supplies the data for this vehicle.
Raises: InitialConnectionError: If the connection to the API could not be established.
75 def shutdown(self) -> None: 76 """ 77 This method will be called when exiting the context manager. 78 79 :return: Nothing 80 :rtype: None 81 """ 82 if not hasattr(self, '_api'): 83 return # Abstract class without API implementation 84 85 if isinstance(self._api, ThreadedAPI): 86 self._api.stop()
This method will be called when exiting the context manager.
Returns
Nothing
123@dataclass 124class Station(object): 125 """ 126 A Station is a stop on the trip 127 """ 128 # noinspection PyTypeHints 129 id: ID 130 """The ID of the station""" 131 name: str 132 """The name of the station""" 133 arrival: ScheduledEvent[datetime] | None 134 """The arrival time at this station""" 135 departure: ScheduledEvent[datetime] | None 136 """The departure time from this station""" 137 position: Position | None 138 """The geographic position of the station""" 139 distance: float | None 140 """The distance from the start to this station""" 141 _connections: Iterable[ConnectingVehicle] 142 143 def __str__(self) -> str: 144 return self.name 145 146 @property 147 def connections(self) -> list[ConnectingVehicle]: 148 """The connecting services departing from this station.""" 149 if not isinstance(self._connections, list): 150 self._connections = list(self._connections) 151 return self._connections 152 153 def calculate_distance(self, other: Station | Position | int | float) -> float | None: 154 """ 155 Calculate the distance in meters between this station and something else. 156 157 Accepts a :class:`Station`, :class:`Position` or a number for the distance calculation. 158 159 :param other: The other station or position to calculate the distance to 160 :type other: Station | Position | int | float 161 :return: The distance in meters 162 :rtype: Optional[float] 163 """ 164 # If there is not enough information to calculate the distance, return None 165 if other is None: 166 return None 167 168 # Both distances since the start are known 169 if isinstance(other, (int, float)) and self.distance is not None: 170 return abs(self.distance - other) 171 172 # Both positions are known 173 if isinstance(other, Position) and self.position is not None: 174 return self.position.calculate_distance(other) 175 176 # Both are a station 177 if isinstance(other, Station): 178 if self.distance is not None and other.distance is not None: 179 return abs(self.distance - other.distance) 180 if self.position is not None and other.position is not None: 181 return self.position.calculate_distance(other.position) 182 # No distance could be determined 183 return None
A Station is a stop on the trip
The arrival time at this station
The departure time from this station
146 @property 147 def connections(self) -> list[ConnectingVehicle]: 148 """The connecting services departing from this station.""" 149 if not isinstance(self._connections, list): 150 self._connections = list(self._connections) 151 return self._connections
The connecting services departing from this station.
153 def calculate_distance(self, other: Station | Position | int | float) -> float | None: 154 """ 155 Calculate the distance in meters between this station and something else. 156 157 Accepts a :class:`Station`, :class:`Position` or a number for the distance calculation. 158 159 :param other: The other station or position to calculate the distance to 160 :type other: Station | Position | int | float 161 :return: The distance in meters 162 :rtype: Optional[float] 163 """ 164 # If there is not enough information to calculate the distance, return None 165 if other is None: 166 return None 167 168 # Both distances since the start are known 169 if isinstance(other, (int, float)) and self.distance is not None: 170 return abs(self.distance - other) 171 172 # Both positions are known 173 if isinstance(other, Position) and self.position is not None: 174 return self.position.calculate_distance(other) 175 176 # Both are a station 177 if isinstance(other, Station): 178 if self.distance is not None and other.distance is not None: 179 return abs(self.distance - other.distance) 180 if self.position is not None and other.position is not None: 181 return self.position.calculate_distance(other.position) 182 # No distance could be determined 183 return None
Calculate the distance in meters between this station and something else.
Accepts a Station, Position or a number for the distance calculation.
Parameters
- other: The other station or position to calculate the distance to
Returns
The distance in meters
107@dataclass 108class ConnectingVehicle(object): 109 """ 110 A connecting vehicle is a vehicle that is not part of the main trip but of a connecting service. 111 It may only have limited information available. 112 """ 113 vehicle_type: str | None 114 """The abbreviated vehicle type""" 115 line_number: str | None 116 """The line number of the vehicle""" 117 departure: ScheduledEvent[datetime] | None 118 """The departure time of the vehicle""" 119 destination: str | None 120 """The destination of the vehicle"""
A connecting vehicle is a vehicle that is not part of the main trip but of a connecting service. It may only have limited information available.
The departure time of the vehicle