onboardapis

onboardapis

Python versions PyPI version License
Build package Deploy documentation

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

Version


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 datetime or timedelta objects 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 the onboardapis.units module 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

GitHub-Pages

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
class Vehicle:
 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

def init(self) -> None:
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.

def shutdown(self) -> None:
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

now: datetime.datetime
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()

Get the current time as seen by the vehicle

Returns

The current time

id: ~ID
 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'
Returns

The ID of the vehicle

Raises
  • DataInvalidError: If the ID could not be fetched from the server
@dataclass
class Station:
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

Station( id: ~ID, name: str, arrival: Optional[onboardapis.data.ScheduledEvent[datetime.datetime]], departure: Optional[onboardapis.data.ScheduledEvent[datetime.datetime]], position: onboardapis.data.Position | None, distance: float | None, _connections: Iterable[ConnectingVehicle])
id: ~ID

The ID of the station

name: str

The name of the station

arrival: Optional[onboardapis.data.ScheduledEvent[datetime.datetime]]

The arrival time at this station

departure: Optional[onboardapis.data.ScheduledEvent[datetime.datetime]]

The departure time from this station

position: onboardapis.data.Position | None

The geographic position of the station

distance: float | None

The distance from the start to this station

connections: list[ConnectingVehicle]
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.

def calculate_distance( self, other: Station | onboardapis.data.Position | int | float) -> float | None:
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

@dataclass
class ConnectingVehicle:
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.

ConnectingVehicle( vehicle_type: str | None, line_number: str | None, departure: Optional[onboardapis.data.ScheduledEvent[datetime.datetime]], destination: str | None)
vehicle_type: str | None

The abbreviated vehicle type

line_number: str | None

The line number of the vehicle

departure: Optional[onboardapis.data.ScheduledEvent[datetime.datetime]]

The departure time of the vehicle

destination: str | None

The destination of the vehicle