onboardapis.mixins

Mixins for vehicles.

The mixins are used to indicate and add functionality to the vehicle classes.

  1"""
  2Mixins for vehicles.
  3
  4The mixins are used to indicate and add functionality to the vehicle classes.
  5"""
  6from __future__ import annotations
  7
  8from abc import ABCMeta, abstractmethod
  9from datetime import timedelta
 10from typing import Generic
 11
 12from .exceptions import DataInvalidError
 13from .data import Position, ID, StationType, API
 14
 15
 16class PositionMixin(metaclass=ABCMeta):
 17    @property
 18    @abstractmethod
 19    def position(self) -> Position:
 20        """
 21        :return: The current position of the vehicle
 22        :raises DataInvalidError: If the position could not be fetched from the server
 23        """
 24        raise NotImplementedError
 25
 26
 27class SpeedMixin(metaclass=ABCMeta):
 28    """
 29    Functionality for a vehicle that provides information about its speed.
 30    """
 31
 32    @property
 33    @abstractmethod
 34    def speed(self) -> float:
 35        r"""
 36        :return: The current speed of the vehicle in $\frac{meters}{second}$
 37        :raises DataInvalidError: If the speed could not be fetched from the server
 38        """
 39        raise NotImplementedError
 40
 41
 42class StationsMixin(Generic[StationType], metaclass=ABCMeta):
 43    """
 44    Functionality for a vehicle that provides information on the journey.
 45    """
 46
 47    def calculate_distance(self, station: StationType) -> float:
 48        """
 49        :return: The distance in meters between the vehicle and a station
 50        :param station: The station to calculate the distance to
 51        :raises DataInvalidError: If the distance between the vehicle and the station could not be calculated
 52                                  due to missing information from the server
 53        :raises NotImplementedError: If the vehicle does not implement ``position`` or ``distance``
 54        """
 55        if hasattr(self, 'position'):
 56            position: Position = getattr(self, 'position')
 57            return station.calculate_distance(position)
 58
 59        if hasattr(self, 'distance'):
 60            distance: float = getattr(self, 'distance')
 61            return station.calculate_distance(distance)
 62
 63        raise NotImplementedError
 64
 65    @property
 66    @abstractmethod
 67    def stations_dict(self) -> dict[ID, StationType]:
 68        """
 69        :return: The stations as a dict of station ID to station instance
 70        :raises DataInvalidError: If the stations could not be fetched from the server
 71        """
 72        raise NotImplementedError
 73
 74    @property
 75    def stations(self) -> list[StationType]:
 76        """
 77        :return: A list that contains every station from `onboardapis.mixins.StationsMixin.stations_dict`.
 78        :raises DataInvalidError: If the stations could not be fetched from the server
 79        """
 80        return list(self.stations_dict.values())
 81
 82    @property
 83    def origin(self) -> StationType:
 84        """
 85        :return: The first station on this trip.
 86        :raises DataInvalidError: If the origin station could not be fetched from the server
 87        """
 88        if len(self.stations) > 0:
 89            return self.stations[0]
 90        raise DataInvalidError("No origin station found!")
 91
 92    @property
 93    @abstractmethod
 94    def current_station(self) -> StationType:
 95        """
 96        :return: The station where this vehicle will arrive next or is currently at
 97        :raises DataInvalidError: If the current station could not be fetched from the server
 98        """
 99        raise NotImplementedError
100
101    @property
102    def destination(self) -> StationType:
103        """
104        :return: The station where this vehicle terminates the current journey
105        :raises DataInvalidError: If the destination station could not be fetched from the server
106        """
107        if len(self.stations) > 0:
108            return self.stations[-1]
109        raise DataInvalidError("No destination station found!")
110
111    @property
112    def delay(self) -> timedelta:
113        """
114        :return: The current delay of the vehicle as a `datetime.timedelta` object.
115        :raises DataInvalidError: If the delay could not be fetched from the server
116        """
117        return timedelta(seconds=(
118            self.current_station.arrival.actual - self.current_station.arrival.scheduled
119        ).total_seconds())
120
121    @property
122    def is_delayed(self) -> bool:
123        """
124        :returns: Whether ``delay`` `> timedelta(seconds=0)`
125        :raises DataInvalidError: If the delay could not be fetched from the server
126        """
127        return self.delay > timedelta()
128
129    @property
130    def distance(self) -> float:
131        """
132        :return: The distance from the start in meters
133        :raises DataInvalidError: If the distance could not be fetched from the server
134        """
135        return self.calculate_distance(self.origin)
136
137
138class InternetAccessMixin(metaclass=ABCMeta):
139    """Adds the internet_access property to a class
140    that defines an :class:`InternetAccessInterface` as ``_internet_access``."""
141    _internet_access: InternetAccessInterface
142
143    @property
144    def internet_access(self) -> InternetAccessInterface:
145        """
146        :return: An interface to manage the internet access for this device
147        """
148        return self._internet_access
149
150
151class InternetAccessInterface(metaclass=ABCMeta):
152    """Interface adding functions for connecting and disconnecting to the internet
153    as well as viewing the current status."""
154
155    _is_enabled: bool = False
156    """Cached information on connection status"""
157    _api: API
158
159    def __init__(self, api: API) -> None:
160        self._api = api
161
162    @abstractmethod
163    def enable(self) -> None:
164        """Enable the internet access for this device.
165
166        **IMPORTANT**:
167            By using this method you automatically agree to the terms of service of the internet access provider.
168
169        :raises ConnectionError: If the internet access is (temporarily) not available.
170        """
171        self._is_enabled = True
172
173    @abstractmethod
174    def disable(self) -> None:
175        """Disable the internet access for this device.
176
177        Disable the internet access for this device by signing out of the captive portal.
178
179        :raises ConnectionError: If the internet access is (temporarily) not available.
180        """
181        if not self.is_enabled:
182            return
183
184        self._is_enabled = False
185
186    @property
187    def is_enabled(self) -> bool:
188        """
189        :return: Whether the internet access is enabled for this device
190        """
191        return self._is_enabled
192
193
194class InternetMetricsInterface(metaclass=ABCMeta):
195    """Interface for information on limited internet access."""
196
197    @property
198    @abstractmethod
199    def limit(self) -> int | float | None:
200        """
201        :return: The total internet access quota in MB or `None` if there is none
202        :raises DataInvalidError: If the quota could not be fetched from the server
203        """
204        raise NotImplementedError
205
206    @property
207    @abstractmethod
208    def used(self) -> int | float | None:
209        """
210        :return: The amount used of the quota in MB or `None` if there is none
211        :raises DataInvalidError: If the usage information could not be fetched from the server
212        """
213        raise NotImplementedError
class PositionMixin:
17class PositionMixin(metaclass=ABCMeta):
18    @property
19    @abstractmethod
20    def position(self) -> Position:
21        """
22        :return: The current position of the vehicle
23        :raises DataInvalidError: If the position could not be fetched from the server
24        """
25        raise NotImplementedError
position: onboardapis.data.Position
18    @property
19    @abstractmethod
20    def position(self) -> Position:
21        """
22        :return: The current position of the vehicle
23        :raises DataInvalidError: If the position could not be fetched from the server
24        """
25        raise NotImplementedError
Returns

The current position of the vehicle

Raises
  • DataInvalidError: If the position could not be fetched from the server
class SpeedMixin:
28class SpeedMixin(metaclass=ABCMeta):
29    """
30    Functionality for a vehicle that provides information about its speed.
31    """
32
33    @property
34    @abstractmethod
35    def speed(self) -> float:
36        r"""
37        :return: The current speed of the vehicle in $\frac{meters}{second}$
38        :raises DataInvalidError: If the speed could not be fetched from the server
39        """
40        raise NotImplementedError

Functionality for a vehicle that provides information about its speed.

speed: float
33    @property
34    @abstractmethod
35    def speed(self) -> float:
36        r"""
37        :return: The current speed of the vehicle in $\frac{meters}{second}$
38        :raises DataInvalidError: If the speed could not be fetched from the server
39        """
40        raise NotImplementedError
Returns

The current speed of the vehicle in $\frac{meters}{second}$

Raises
  • DataInvalidError: If the speed could not be fetched from the server
class StationsMixin(typing.Generic[~StationType]):
 43class StationsMixin(Generic[StationType], metaclass=ABCMeta):
 44    """
 45    Functionality for a vehicle that provides information on the journey.
 46    """
 47
 48    def calculate_distance(self, station: StationType) -> float:
 49        """
 50        :return: The distance in meters between the vehicle and a station
 51        :param station: The station to calculate the distance to
 52        :raises DataInvalidError: If the distance between the vehicle and the station could not be calculated
 53                                  due to missing information from the server
 54        :raises NotImplementedError: If the vehicle does not implement ``position`` or ``distance``
 55        """
 56        if hasattr(self, 'position'):
 57            position: Position = getattr(self, 'position')
 58            return station.calculate_distance(position)
 59
 60        if hasattr(self, 'distance'):
 61            distance: float = getattr(self, 'distance')
 62            return station.calculate_distance(distance)
 63
 64        raise NotImplementedError
 65
 66    @property
 67    @abstractmethod
 68    def stations_dict(self) -> dict[ID, StationType]:
 69        """
 70        :return: The stations as a dict of station ID to station instance
 71        :raises DataInvalidError: If the stations could not be fetched from the server
 72        """
 73        raise NotImplementedError
 74
 75    @property
 76    def stations(self) -> list[StationType]:
 77        """
 78        :return: A list that contains every station from `onboardapis.mixins.StationsMixin.stations_dict`.
 79        :raises DataInvalidError: If the stations could not be fetched from the server
 80        """
 81        return list(self.stations_dict.values())
 82
 83    @property
 84    def origin(self) -> StationType:
 85        """
 86        :return: The first station on this trip.
 87        :raises DataInvalidError: If the origin station could not be fetched from the server
 88        """
 89        if len(self.stations) > 0:
 90            return self.stations[0]
 91        raise DataInvalidError("No origin station found!")
 92
 93    @property
 94    @abstractmethod
 95    def current_station(self) -> StationType:
 96        """
 97        :return: The station where this vehicle will arrive next or is currently at
 98        :raises DataInvalidError: If the current station could not be fetched from the server
 99        """
100        raise NotImplementedError
101
102    @property
103    def destination(self) -> StationType:
104        """
105        :return: The station where this vehicle terminates the current journey
106        :raises DataInvalidError: If the destination station could not be fetched from the server
107        """
108        if len(self.stations) > 0:
109            return self.stations[-1]
110        raise DataInvalidError("No destination station found!")
111
112    @property
113    def delay(self) -> timedelta:
114        """
115        :return: The current delay of the vehicle as a `datetime.timedelta` object.
116        :raises DataInvalidError: If the delay could not be fetched from the server
117        """
118        return timedelta(seconds=(
119            self.current_station.arrival.actual - self.current_station.arrival.scheduled
120        ).total_seconds())
121
122    @property
123    def is_delayed(self) -> bool:
124        """
125        :returns: Whether ``delay`` `> timedelta(seconds=0)`
126        :raises DataInvalidError: If the delay could not be fetched from the server
127        """
128        return self.delay > timedelta()
129
130    @property
131    def distance(self) -> float:
132        """
133        :return: The distance from the start in meters
134        :raises DataInvalidError: If the distance could not be fetched from the server
135        """
136        return self.calculate_distance(self.origin)

Functionality for a vehicle that provides information on the journey.

def calculate_distance(self, station: ~StationType) -> float:
48    def calculate_distance(self, station: StationType) -> float:
49        """
50        :return: The distance in meters between the vehicle and a station
51        :param station: The station to calculate the distance to
52        :raises DataInvalidError: If the distance between the vehicle and the station could not be calculated
53                                  due to missing information from the server
54        :raises NotImplementedError: If the vehicle does not implement ``position`` or ``distance``
55        """
56        if hasattr(self, 'position'):
57            position: Position = getattr(self, 'position')
58            return station.calculate_distance(position)
59
60        if hasattr(self, 'distance'):
61            distance: float = getattr(self, 'distance')
62            return station.calculate_distance(distance)
63
64        raise NotImplementedError
Returns

The distance in meters between the vehicle and a station

Parameters
  • station: The station to calculate the distance to
Raises
  • DataInvalidError: If the distance between the vehicle and the station could not be calculated due to missing information from the server
  • NotImplementedError: If the vehicle does not implement position or distance
stations_dict: dict[~ID, ~StationType]
66    @property
67    @abstractmethod
68    def stations_dict(self) -> dict[ID, StationType]:
69        """
70        :return: The stations as a dict of station ID to station instance
71        :raises DataInvalidError: If the stations could not be fetched from the server
72        """
73        raise NotImplementedError
Returns

The stations as a dict of station ID to station instance

Raises
  • DataInvalidError: If the stations could not be fetched from the server
stations: list[~StationType]
75    @property
76    def stations(self) -> list[StationType]:
77        """
78        :return: A list that contains every station from `onboardapis.mixins.StationsMixin.stations_dict`.
79        :raises DataInvalidError: If the stations could not be fetched from the server
80        """
81        return list(self.stations_dict.values())
Returns

A list that contains every station from onboardapis.mixins.StationsMixin.stations_dict.

Raises
  • DataInvalidError: If the stations could not be fetched from the server
origin: ~StationType
83    @property
84    def origin(self) -> StationType:
85        """
86        :return: The first station on this trip.
87        :raises DataInvalidError: If the origin station could not be fetched from the server
88        """
89        if len(self.stations) > 0:
90            return self.stations[0]
91        raise DataInvalidError("No origin station found!")
Returns

The first station on this trip.

Raises
  • DataInvalidError: If the origin station could not be fetched from the server
current_station: ~StationType
 93    @property
 94    @abstractmethod
 95    def current_station(self) -> StationType:
 96        """
 97        :return: The station where this vehicle will arrive next or is currently at
 98        :raises DataInvalidError: If the current station could not be fetched from the server
 99        """
100        raise NotImplementedError
Returns

The station where this vehicle will arrive next or is currently at

Raises
  • DataInvalidError: If the current station could not be fetched from the server
destination: ~StationType
102    @property
103    def destination(self) -> StationType:
104        """
105        :return: The station where this vehicle terminates the current journey
106        :raises DataInvalidError: If the destination station could not be fetched from the server
107        """
108        if len(self.stations) > 0:
109            return self.stations[-1]
110        raise DataInvalidError("No destination station found!")
Returns

The station where this vehicle terminates the current journey

Raises
  • DataInvalidError: If the destination station could not be fetched from the server
delay: datetime.timedelta
112    @property
113    def delay(self) -> timedelta:
114        """
115        :return: The current delay of the vehicle as a `datetime.timedelta` object.
116        :raises DataInvalidError: If the delay could not be fetched from the server
117        """
118        return timedelta(seconds=(
119            self.current_station.arrival.actual - self.current_station.arrival.scheduled
120        ).total_seconds())
Returns

The current delay of the vehicle as a datetime.timedelta object.

Raises
  • DataInvalidError: If the delay could not be fetched from the server
is_delayed: bool
122    @property
123    def is_delayed(self) -> bool:
124        """
125        :returns: Whether ``delay`` `> timedelta(seconds=0)`
126        :raises DataInvalidError: If the delay could not be fetched from the server
127        """
128        return self.delay > timedelta()

:returns: Whether delay > timedelta(seconds=0)

Raises
  • DataInvalidError: If the delay could not be fetched from the server
distance: float
130    @property
131    def distance(self) -> float:
132        """
133        :return: The distance from the start in meters
134        :raises DataInvalidError: If the distance could not be fetched from the server
135        """
136        return self.calculate_distance(self.origin)
Returns

The distance from the start in meters

Raises
  • DataInvalidError: If the distance could not be fetched from the server
class InternetAccessMixin:
139class InternetAccessMixin(metaclass=ABCMeta):
140    """Adds the internet_access property to a class
141    that defines an :class:`InternetAccessInterface` as ``_internet_access``."""
142    _internet_access: InternetAccessInterface
143
144    @property
145    def internet_access(self) -> InternetAccessInterface:
146        """
147        :return: An interface to manage the internet access for this device
148        """
149        return self._internet_access

Adds the internet_access property to a class that defines an InternetAccessInterface as _internet_access.

internet_access: InternetAccessInterface
144    @property
145    def internet_access(self) -> InternetAccessInterface:
146        """
147        :return: An interface to manage the internet access for this device
148        """
149        return self._internet_access
Returns

An interface to manage the internet access for this device

class InternetAccessInterface:
152class InternetAccessInterface(metaclass=ABCMeta):
153    """Interface adding functions for connecting and disconnecting to the internet
154    as well as viewing the current status."""
155
156    _is_enabled: bool = False
157    """Cached information on connection status"""
158    _api: API
159
160    def __init__(self, api: API) -> None:
161        self._api = api
162
163    @abstractmethod
164    def enable(self) -> None:
165        """Enable the internet access for this device.
166
167        **IMPORTANT**:
168            By using this method you automatically agree to the terms of service of the internet access provider.
169
170        :raises ConnectionError: If the internet access is (temporarily) not available.
171        """
172        self._is_enabled = True
173
174    @abstractmethod
175    def disable(self) -> None:
176        """Disable the internet access for this device.
177
178        Disable the internet access for this device by signing out of the captive portal.
179
180        :raises ConnectionError: If the internet access is (temporarily) not available.
181        """
182        if not self.is_enabled:
183            return
184
185        self._is_enabled = False
186
187    @property
188    def is_enabled(self) -> bool:
189        """
190        :return: Whether the internet access is enabled for this device
191        """
192        return self._is_enabled

Interface adding functions for connecting and disconnecting to the internet as well as viewing the current status.

@abstractmethod
def enable(self) -> None:
163    @abstractmethod
164    def enable(self) -> None:
165        """Enable the internet access for this device.
166
167        **IMPORTANT**:
168            By using this method you automatically agree to the terms of service of the internet access provider.
169
170        :raises ConnectionError: If the internet access is (temporarily) not available.
171        """
172        self._is_enabled = True

Enable the internet access for this device.

IMPORTANT: By using this method you automatically agree to the terms of service of the internet access provider.

Raises
  • ConnectionError: If the internet access is (temporarily) not available.
@abstractmethod
def disable(self) -> None:
174    @abstractmethod
175    def disable(self) -> None:
176        """Disable the internet access for this device.
177
178        Disable the internet access for this device by signing out of the captive portal.
179
180        :raises ConnectionError: If the internet access is (temporarily) not available.
181        """
182        if not self.is_enabled:
183            return
184
185        self._is_enabled = False

Disable the internet access for this device.

Disable the internet access for this device by signing out of the captive portal.

Raises
  • ConnectionError: If the internet access is (temporarily) not available.
is_enabled: bool
187    @property
188    def is_enabled(self) -> bool:
189        """
190        :return: Whether the internet access is enabled for this device
191        """
192        return self._is_enabled
Returns

Whether the internet access is enabled for this device

class InternetMetricsInterface:
195class InternetMetricsInterface(metaclass=ABCMeta):
196    """Interface for information on limited internet access."""
197
198    @property
199    @abstractmethod
200    def limit(self) -> int | float | None:
201        """
202        :return: The total internet access quota in MB or `None` if there is none
203        :raises DataInvalidError: If the quota could not be fetched from the server
204        """
205        raise NotImplementedError
206
207    @property
208    @abstractmethod
209    def used(self) -> int | float | None:
210        """
211        :return: The amount used of the quota in MB or `None` if there is none
212        :raises DataInvalidError: If the usage information could not be fetched from the server
213        """
214        raise NotImplementedError

Interface for information on limited internet access.

limit: int | float | None
198    @property
199    @abstractmethod
200    def limit(self) -> int | float | None:
201        """
202        :return: The total internet access quota in MB or `None` if there is none
203        :raises DataInvalidError: If the quota could not be fetched from the server
204        """
205        raise NotImplementedError
Returns

The total internet access quota in MB or None if there is none

Raises
  • DataInvalidError: If the quota could not be fetched from the server
used: int | float | None
207    @property
208    @abstractmethod
209    def used(self) -> int | float | None:
210        """
211        :return: The amount used of the quota in MB or `None` if there is none
212        :raises DataInvalidError: If the usage information could not be fetched from the server
213        """
214        raise NotImplementedError
Returns

The amount used of the quota in MB or None if there is none

Raises
  • DataInvalidError: If the usage information could not be fetched from the server