python-openpublictransport 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. openpublictransport/__init__.py +5 -0
  2. openpublictransport/const.py +75 -0
  3. openpublictransport/models.py +95 -0
  4. openpublictransport/parsers.py +122 -0
  5. openpublictransport/providers/__init__.py +127 -0
  6. openpublictransport/providers/avv.py +44 -0
  7. openpublictransport/providers/base.py +77 -0
  8. openpublictransport/providers/beg.py +46 -0
  9. openpublictransport/providers/bsvg.py +44 -0
  10. openpublictransport/providers/bvg.py +32 -0
  11. openpublictransport/providers/db.py +39 -0
  12. openpublictransport/providers/ding.py +44 -0
  13. openpublictransport/providers/efa_base.py +209 -0
  14. openpublictransport/providers/fptf_base.py +169 -0
  15. openpublictransport/providers/gtfsde.py +21 -0
  16. openpublictransport/providers/hvv.py +40 -0
  17. openpublictransport/providers/kvv.py +38 -0
  18. openpublictransport/providers/mvv.py +48 -0
  19. openpublictransport/providers/nta.py +288 -0
  20. openpublictransport/providers/nvbw.py +44 -0
  21. openpublictransport/providers/nwl.py +46 -0
  22. openpublictransport/providers/oebb.py +182 -0
  23. openpublictransport/providers/otp.py +378 -0
  24. openpublictransport/providers/otp_base.py +268 -0
  25. openpublictransport/providers/otp_custom.py +22 -0
  26. openpublictransport/providers/rmv.py +272 -0
  27. openpublictransport/providers/rvv.py +43 -0
  28. openpublictransport/providers/sbb.py +174 -0
  29. openpublictransport/providers/trafiklab.py +280 -0
  30. openpublictransport/providers/transitous.py +198 -0
  31. openpublictransport/providers/trias_base.py +323 -0
  32. openpublictransport/providers/vagfr.py +44 -0
  33. openpublictransport/providers/vbn.py +73 -0
  34. openpublictransport/providers/vgn.py +46 -0
  35. openpublictransport/providers/vrn.py +51 -0
  36. openpublictransport/providers/vrr.py +51 -0
  37. openpublictransport/providers/vvo.py +45 -0
  38. openpublictransport/providers/vvs.py +48 -0
  39. python_openpublictransport-0.1.0.dist-info/METADATA +14 -0
  40. python_openpublictransport-0.1.0.dist-info/RECORD +42 -0
  41. python_openpublictransport-0.1.0.dist-info/WHEEL +5 -0
  42. python_openpublictransport-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,39 @@
1
+ """Deutsche Bahn provider implementation.
2
+
3
+ Uses the v6.db.transport.rest API (FPTF format).
4
+ No API key required.
5
+
6
+ NOTE: This API is a community-maintained proxy (by derhuerst).
7
+ It is free and open but not officially supported by Deutsche Bahn.
8
+ Availability is not guaranteed — the API may experience occasional downtime.
9
+ """
10
+
11
+ from ..const import PROVIDER_DB
12
+ from .fptf_base import FPTFBaseProvider
13
+
14
+
15
+ class DBProvider(FPTFBaseProvider):
16
+ """Deutsche Bahn provider using v6.db.transport.rest API."""
17
+
18
+ API_BASE = "https://v6.db.transport.rest"
19
+
20
+ PRODUCT_MAPPING = {
21
+ "nationalExpress": "train", # ICE
22
+ "national": "train", # IC/EC
23
+ "regionalExpress": "train", # RE
24
+ "regional": "train", # RB
25
+ "suburban": "train", # S-Bahn
26
+ "subway": "subway", # U-Bahn
27
+ "tram": "tram",
28
+ "bus": "bus",
29
+ "ferry": "ferry",
30
+ "taxi": "taxi",
31
+ }
32
+
33
+ @property
34
+ def provider_id(self) -> str:
35
+ return PROVIDER_DB
36
+
37
+ @property
38
+ def provider_name(self) -> str:
39
+ return "DB (Deutsche Bahn)"
@@ -0,0 +1,44 @@
1
+ """DING (Donau-Iller-Nahverkehrsverbund) provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import PROVIDER_DING
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class DINGProvider(EFABaseProvider):
10
+ """DING (Ulm) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_DING
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "DING (Ulm)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://www.ding.eu/ding3/XML_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://www.ding.eu/ding3/XML_STOPFINDER_REQUEST"
27
+
28
+ def get_timezone(self) -> str:
29
+ return "Europe/Berlin"
30
+
31
+ def get_transport_type_mapping(self) -> Dict[Any, str]:
32
+ return {
33
+ 0: "train", # High-speed trains (ICE, IC, EC)
34
+ 1: "train", # Regional trains (RE, RB, S-Bahn)
35
+ 4: "tram", # Straßenbahn
36
+ 5: "bus", # City bus
37
+ 6: "bus", # Regional bus
38
+ 7: "bus", # Express bus
39
+ 8: "bus", # Night bus
40
+ 13: "train", # Regionalzug (RE)
41
+ }
42
+
43
+ def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
44
+ return lambda s, est, plan: est != plan if est and plan else False
@@ -0,0 +1,209 @@
1
+ """Base class for EFA (Electronic Fahrplan-Auskunft) providers."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from abc import abstractmethod
6
+ from datetime import datetime
7
+ from typing import Any, Callable, Dict, List, Optional, Union
8
+ from urllib.parse import quote
9
+ from zoneinfo import ZoneInfo
10
+
11
+ import aiohttp
12
+
13
+ from ..models import UnifiedDeparture
14
+ from ..parsers import parse_departure_generic
15
+ from .base import BaseProvider
16
+
17
+ _LOGGER = logging.getLogger(__name__)
18
+
19
+
20
+ class EFABaseProvider(BaseProvider):
21
+ """Base class for all EFA-based providers (VRR, KVV, HVV, MVV, etc.)."""
22
+
23
+ @property
24
+ @abstractmethod
25
+ def dm_base_url(self) -> str:
26
+ """Return the base URL for departure monitor requests."""
27
+
28
+ @property
29
+ @abstractmethod
30
+ def sf_base_url(self) -> str:
31
+ """Return the base URL for stop finder requests."""
32
+
33
+ def get_platform_fn(self) -> Callable[[Dict[str, Any]], str]:
34
+ """Return function to extract platform from stop event."""
35
+ return lambda s: s.get("platform", {}).get("name") or s.get("platformName", "")
36
+
37
+ def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
38
+ """Return function to detect realtime data."""
39
+ return lambda s, est, plan: "MONITORED" in s.get("realtimeStatus", [])
40
+
41
+ async def fetch_departures(
42
+ self,
43
+ station_id: Optional[str],
44
+ place_dm: str,
45
+ name_dm: str,
46
+ departures_limit: int,
47
+ ) -> Optional[Dict[str, Any]]:
48
+ """Fetch departure data from EFA API."""
49
+ if station_id:
50
+ params = (
51
+ f"outputFormat=RapidJSON&"
52
+ f"stateless=1&"
53
+ f"type_dm=any&"
54
+ f"name_dm={station_id}&"
55
+ f"mode=direct&"
56
+ f"useRealtime=1&"
57
+ f"limit={departures_limit}"
58
+ )
59
+ else:
60
+ params = (
61
+ f"outputFormat=RapidJSON&"
62
+ f"place_dm={place_dm}&"
63
+ f"type_dm=stop&"
64
+ f"name_dm={name_dm}&"
65
+ f"mode=direct&"
66
+ f"useRealtime=1&"
67
+ f"limit={departures_limit}"
68
+ )
69
+
70
+ url = f"{self.dm_base_url}?{params}"
71
+ name = self.provider_name
72
+
73
+ headers = {"User-Agent": f"Mozilla/5.0 (compatible; OpenPublicTransport {self.provider_id.upper()})"}
74
+
75
+ max_retries = 3
76
+ for attempt in range(1, max_retries + 1):
77
+ try:
78
+ async with self.session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as response:
79
+ if response.status == 200:
80
+ try:
81
+ json_data = await response.json()
82
+ if not isinstance(json_data, dict):
83
+ _LOGGER.warning("%s API returned non-dict response: %s", name, type(json_data))
84
+ return None
85
+
86
+ if "stopEvents" not in json_data:
87
+ _LOGGER.debug("%s API response missing 'stopEvents' field", name)
88
+ return {"stopEvents": []}
89
+
90
+ return json_data
91
+ except (ValueError, aiohttp.ContentTypeError) as e:
92
+ _LOGGER.warning("%s API returned invalid JSON: %s", name, e)
93
+ return None
94
+ except Exception as e:
95
+ _LOGGER.warning("%s API JSON parsing failed: %s", name, e)
96
+ return None
97
+ elif response.status == 404:
98
+ _LOGGER.warning("%s API endpoint not found (404)", name)
99
+ return None
100
+ elif response.status >= 500:
101
+ _LOGGER.warning("%s API server error (status %s)", name, response.status)
102
+ else:
103
+ _LOGGER.warning("%s API returned status %s", name, response.status)
104
+
105
+ except asyncio.TimeoutError:
106
+ _LOGGER.warning("%s API timeout on attempt %s", name, attempt)
107
+ except Exception as e:
108
+ _LOGGER.warning("%s attempt %s failed: %s", name, attempt, e)
109
+
110
+ if attempt < max_retries:
111
+ await asyncio.sleep(2**attempt)
112
+
113
+ return None
114
+
115
+ def parse_departure(
116
+ self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
117
+ ) -> Optional[UnifiedDeparture]:
118
+ """Parse a single departure from EFA API response."""
119
+ type_mapping = self.get_transport_type_mapping()
120
+
121
+ def determine_transport_type(transportation: Dict[str, Any]) -> str:
122
+ product = transportation.get("product", {})
123
+ product_class = product.get("class", 0)
124
+ transport_type = type_mapping.get(product_class, "unknown")
125
+ if transport_type == "unknown":
126
+ _LOGGER.debug(
127
+ "Unknown transport class %s for line %s",
128
+ product_class,
129
+ transportation.get("number", "unknown"),
130
+ )
131
+ return transport_type
132
+
133
+ return parse_departure_generic(
134
+ stop,
135
+ tz,
136
+ now,
137
+ get_transport_type_fn=determine_transport_type,
138
+ get_platform_fn=self.get_platform_fn(),
139
+ get_realtime_fn=self.get_realtime_fn(),
140
+ )
141
+
142
+ async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
143
+ """Search for stops using EFA Stopfinder API."""
144
+ if "," in search_term:
145
+ parts = search_term.split(",", 1)
146
+ stop_name = parts[0].strip()
147
+ place_name = parts[1].strip()
148
+ params = (
149
+ f"outputFormat=RapidJSON&"
150
+ f"locationServerActive=1&"
151
+ f"type_sf=any&"
152
+ f"name_sf={quote(stop_name, safe='')}&"
153
+ f"place_sf={quote(place_name, safe='')}&"
154
+ f"SpEncId=0"
155
+ )
156
+ else:
157
+ params = (
158
+ f"outputFormat=RapidJSON&"
159
+ f"locationServerActive=1&"
160
+ f"type_sf=stop&"
161
+ f"name_sf={quote(search_term, safe='')}&"
162
+ f"SpEncId=0"
163
+ )
164
+
165
+ url = f"{self.sf_base_url}?{params}"
166
+ name = self.provider_name
167
+
168
+ try:
169
+ async with self.session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
170
+ if response.status == 200:
171
+ try:
172
+ data = await response.json()
173
+ except (ValueError, aiohttp.ContentTypeError) as e:
174
+ _LOGGER.error("Invalid JSON response from %s API: %s", name, e)
175
+ return []
176
+
177
+ if not isinstance(data, dict):
178
+ _LOGGER.error("%s API returned non-dict response: %s", name, type(data))
179
+ return []
180
+
181
+ locations = data.get("locations", [])
182
+ results = []
183
+
184
+ for location in locations:
185
+ if not isinstance(location, dict):
186
+ continue
187
+
188
+ disassembled_name = location.get("disassembledName", "")
189
+ place = ""
190
+ if "," in disassembled_name:
191
+ parts = disassembled_name.rsplit(",", 1)
192
+ place = parts[-1].strip() if len(parts) > 1 else ""
193
+
194
+ results.append(
195
+ {
196
+ "id": location.get("id", ""),
197
+ "name": location.get("name", ""),
198
+ "place": place,
199
+ "area_type": location.get("type", ""),
200
+ }
201
+ )
202
+
203
+ return results
204
+ else:
205
+ _LOGGER.error("%s API returned status %s", name, response.status)
206
+ except Exception as e:
207
+ _LOGGER.error("Error searching %s stops: %s", name, e, exc_info=True)
208
+
209
+ return []
@@ -0,0 +1,169 @@
1
+ """Base provider for FPTF (Friendly Public Transport Format) APIs."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Optional, Union
6
+ from urllib.parse import quote
7
+ from zoneinfo import ZoneInfo
8
+
9
+ import aiohttp
10
+
11
+ from ..models import UnifiedDeparture
12
+ from .base import BaseProvider
13
+
14
+ _LOGGER = logging.getLogger(__name__)
15
+
16
+
17
+ def _parse_dt(s: str) -> Optional[datetime]:
18
+ """Parse an ISO datetime string, returning None on failure."""
19
+ try:
20
+ return datetime.fromisoformat(s)
21
+ except (ValueError, TypeError):
22
+ return None
23
+
24
+
25
+ class FPTFBaseProvider(BaseProvider):
26
+ """Base class for FPTF-based providers (transport.rest APIs)."""
27
+
28
+ API_BASE: str = ""
29
+ PRODUCT_MAPPING: Dict[str, str] = {}
30
+ DEFAULT_TRANSPORT_TYPE: str = "train"
31
+
32
+ def get_timezone(self) -> str:
33
+ return "Europe/Berlin"
34
+
35
+ async def fetch_departures(
36
+ self,
37
+ station_id: Optional[str],
38
+ place_dm: str,
39
+ name_dm: str,
40
+ departures_limit: int,
41
+ ) -> Optional[Dict[str, Any]]:
42
+ if not station_id:
43
+ _LOGGER.warning("%s provider requires a station_id", self.provider_name)
44
+ return None
45
+
46
+ url = f"{self.API_BASE}/stops/{station_id}/departures?results={departures_limit}&duration=120"
47
+
48
+ try:
49
+ async with self.session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as response:
50
+ if response.status == 200:
51
+ data = await response.json()
52
+ if not isinstance(data, dict) or "departures" not in data:
53
+ _LOGGER.warning("%s API unexpected response format", self.provider_name)
54
+ return {"stopEvents": []}
55
+ return {"stopEvents": data["departures"]}
56
+ else:
57
+ _LOGGER.warning("%s API returned status %s", self.provider_name, response.status)
58
+ except aiohttp.ClientError as e:
59
+ _LOGGER.warning("%s API request failed: %s", self.provider_name, e)
60
+ except Exception as e:
61
+ _LOGGER.warning("%s API error: %s", self.provider_name, e)
62
+
63
+ return None
64
+
65
+ def parse_departure(
66
+ self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
67
+ ) -> Optional[UnifiedDeparture]:
68
+ try:
69
+ when_str = stop.get("when") or stop.get("plannedWhen")
70
+ planned_str = stop.get("plannedWhen")
71
+ if not when_str or not planned_str:
72
+ return None
73
+
74
+ when = _parse_dt(when_str)
75
+ planned = _parse_dt(planned_str)
76
+ if not when or not planned:
77
+ return None
78
+
79
+ when_local = when.astimezone(tz)
80
+ planned_local = planned.astimezone(tz)
81
+
82
+ delay_seconds = stop.get("delay") or 0
83
+ delay_minutes = int(delay_seconds / 60)
84
+
85
+ line_info = stop.get("line", {})
86
+ line_name = line_info.get("name", "")
87
+ product = line_info.get("product", "")
88
+ transport_type = self.PRODUCT_MAPPING.get(product, self.DEFAULT_TRANSPORT_TYPE)
89
+
90
+ destination_info = stop.get("destination", {})
91
+ destination = destination_info.get("name", stop.get("direction", "Unknown"))
92
+
93
+ platform = stop.get("platform") or ""
94
+ planned_platform = stop.get("plannedPlatform") or ""
95
+ platform_changed = bool(platform and planned_platform and platform != planned_platform)
96
+
97
+ time_diff = when_local - now
98
+ minutes_until = max(0, int(time_diff.total_seconds() / 60))
99
+
100
+ is_realtime = stop.get("prognosisType") is not None
101
+
102
+ notices = []
103
+ for remark in stop.get("remarks", []):
104
+ if isinstance(remark, dict) and remark.get("type") == "warning":
105
+ text = remark.get("text") or remark.get("summary", "")
106
+ if text:
107
+ notices.append(text)
108
+
109
+ operator = line_info.get("operator", {})
110
+ agency = operator.get("name") if isinstance(operator, dict) else None
111
+
112
+ return UnifiedDeparture(
113
+ line=line_name,
114
+ destination=destination,
115
+ departure_time=when_local.strftime("%H:%M"),
116
+ planned_time=planned_local.strftime("%H:%M"),
117
+ delay=delay_minutes,
118
+ platform=platform,
119
+ transportation_type=transport_type,
120
+ is_realtime=is_realtime,
121
+ minutes_until_departure=minutes_until,
122
+ departure_time_obj=when_local,
123
+ description=stop.get("direction"),
124
+ agency=agency,
125
+ notices=notices if notices else None,
126
+ planned_platform=planned_platform if platform_changed else None,
127
+ platform_changed=platform_changed,
128
+ )
129
+ except Exception as e:
130
+ _LOGGER.debug("Error parsing %s departure: %s", self.provider_name, e)
131
+ return None
132
+
133
+ async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
134
+ url = f"{self.API_BASE}/locations?query={quote(search_term, safe='')}&results=15"
135
+
136
+ try:
137
+ async with self.session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
138
+ if response.status == 200:
139
+ data = await response.json()
140
+ if not isinstance(data, list):
141
+ return []
142
+
143
+ results = []
144
+ for location in data:
145
+ if not isinstance(location, dict):
146
+ continue
147
+ if location.get("type") not in ("stop", "station"):
148
+ continue
149
+
150
+ name = location.get("name", "")
151
+ place = ""
152
+ if "(" in name and name.endswith(")"):
153
+ place = name[name.rfind("(") + 1 : -1]
154
+
155
+ results.append(
156
+ {
157
+ "id": location.get("id", ""),
158
+ "name": name,
159
+ "place": place,
160
+ "area_type": "stop",
161
+ }
162
+ )
163
+ return results
164
+ else:
165
+ _LOGGER.error("%s API returned status %s", self.provider_name, response.status)
166
+ except Exception as e:
167
+ _LOGGER.error("Error searching %s stops: %s", self.provider_name, e)
168
+
169
+ return []
@@ -0,0 +1,21 @@
1
+ """Community OTP2 provider — api.openpublictransport.net (GTFS.DE data, Germany-wide)."""
2
+
3
+ from .otp import OTPProvider
4
+
5
+
6
+ class OPTProvider(OTPProvider):
7
+ """Community server at api.openpublictransport.net."""
8
+
9
+ otp_base_url = "https://api.openpublictransport.net/otp/routers/default"
10
+
11
+ @property
12
+ def provider_id(self) -> str:
13
+ return "openpublictransport"
14
+
15
+ @property
16
+ def provider_name(self) -> str:
17
+ return "openpublictransport.net (Deutschlandweit)"
18
+
19
+ @property
20
+ def requires_api_key(self) -> bool:
21
+ return True
@@ -0,0 +1,40 @@
1
+ """HVV (Hamburger Verkehrsverbund) provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import HVV_TRANSPORTATION_TYPES, PROVIDER_HVV
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class HVVProvider(EFABaseProvider):
10
+ """HVV (Hamburger Verkehrsverbund) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_HVV
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "HVV (Hamburg)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://hvv.efa.de/efa/XML_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://hvv.efa.de/efa/XML_STOPFINDER_REQUEST"
27
+
28
+ def get_timezone(self) -> str:
29
+ return "Europe/Berlin"
30
+
31
+ def get_transport_type_mapping(self) -> Dict[Any, str]:
32
+ return HVV_TRANSPORTATION_TYPES
33
+
34
+ def get_platform_fn(self) -> Callable[[Dict[str, Any]], str]:
35
+ return lambda s: (
36
+ s.get("location", {}).get("properties", {}).get("platform") or s.get("location", {}).get("platformName", "")
37
+ )
38
+
39
+ def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
40
+ return lambda s, est, plan: est != plan if est and plan else False
@@ -0,0 +1,38 @@
1
+ """KVV (Karlsruher Verkehrsverbund) provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import KVV_TRANSPORTATION_TYPES, PROVIDER_KVV
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class KVVProvider(EFABaseProvider):
10
+ """KVV (Karlsruher Verkehrsverbund) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_KVV
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "KVV (Karlsruhe)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://projekte.kvv-efa.de/sl3-alone/XSLT_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://projekte.kvv-efa.de/sl3-alone/XML_STOPFINDER_REQUEST"
27
+
28
+ def get_timezone(self) -> str:
29
+ return "Europe/Berlin"
30
+
31
+ def get_transport_type_mapping(self) -> Dict[Any, str]:
32
+ return KVV_TRANSPORTATION_TYPES
33
+
34
+ def get_platform_fn(self) -> Callable[[Dict[str, Any]], str]:
35
+ return lambda s: s.get("location", {}).get("disassembledName") or s.get("platformName", "")
36
+
37
+ def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
38
+ return lambda s, est, plan: s.get("isRealtimeControlled", False)
@@ -0,0 +1,48 @@
1
+ """MVV (Münchner Verkehrs- und Tarifverbund) provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import PROVIDER_MVV
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class MVVProvider(EFABaseProvider):
10
+ """MVV (München) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_MVV
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "MVV (München)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://efa.mvv-muenchen.de/ng/XML_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://efa.mvv-muenchen.de/ng/XML_STOPFINDER_REQUEST"
27
+
28
+ def get_timezone(self) -> str:
29
+ return "Europe/Berlin"
30
+
31
+ def get_transport_type_mapping(self) -> Dict[Any, str]:
32
+ return {
33
+ 0: "train", # Fernverkehr (ICE, IC, EC)
34
+ 1: "train", # S-Bahn
35
+ 2: "subway", # U-Bahn
36
+ 3: "subway", # U-Bahn variant
37
+ 4: "tram", # Tram
38
+ 5: "bus", # Stadtbus
39
+ 6: "bus", # Regionalbus
40
+ 7: "bus", # Schnellbus
41
+ 8: "bus", # Nachtbus
42
+ 9: "ferry", # Fähre
43
+ 10: "taxi", # Rufbus/Taxi
44
+ 13: "train", # Regionalzug (RE/RB)
45
+ }
46
+
47
+ def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
48
+ return lambda s, est, plan: est != plan if est and plan else False