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,323 @@
1
+ """Base provider for TRIAS (VDV 431-2) protocol APIs."""
2
+
3
+ import logging
4
+ from datetime import datetime
5
+ from typing import Any, Dict, List, Optional, Union
6
+ from xml.etree import ElementTree as ET
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
+ NS = {
17
+ "trias": "http://www.vdv.de/trias",
18
+ "siri": "http://www.siri.org.uk/siri",
19
+ }
20
+
21
+
22
+ def _parse_dt(s: str) -> Optional[datetime]:
23
+ """Parse an ISO datetime string, returning None on failure."""
24
+ try:
25
+ return datetime.fromisoformat(s)
26
+ except (ValueError, TypeError):
27
+ return None
28
+
29
+
30
+ def _find(element: Optional[ET.Element], path: str) -> Optional[ET.Element]:
31
+ if element is None:
32
+ return None
33
+ return element.find(path, NS)
34
+
35
+
36
+ def _findall(element: Optional[ET.Element], path: str) -> List[ET.Element]:
37
+ if element is None:
38
+ return []
39
+ return element.findall(path, NS)
40
+
41
+
42
+ def _text(element: Optional[ET.Element], path: str, default: str = "") -> str:
43
+ if element is None:
44
+ return default
45
+ child = element.find(path, NS)
46
+ return child.text if child is not None and child.text else default
47
+
48
+
49
+ DEFAULT_MODE_MAPPING = {
50
+ "rail": "train",
51
+ "urbanRail": "train",
52
+ "metro": "subway",
53
+ "underground": "subway",
54
+ "tram": "tram",
55
+ "bus": "bus",
56
+ "coach": "bus",
57
+ "water": "ferry",
58
+ "telecabin": "tram",
59
+ "funicular": "train",
60
+ "taxi": "taxi",
61
+ }
62
+
63
+
64
+ class TRIASBaseProvider(BaseProvider):
65
+ """Base class for TRIAS-based providers."""
66
+
67
+ trias_base_url: str = ""
68
+ requestor_ref: str = "openpublictransport"
69
+ trias_version: str = "1.1"
70
+
71
+ def get_timezone(self) -> str:
72
+ return "Europe/Berlin"
73
+
74
+ def get_mode_mapping(self) -> Dict[str, str]:
75
+ return DEFAULT_MODE_MAPPING
76
+
77
+ def _build_stop_event_request(self, stop_id: str, limit: int) -> str:
78
+ now = datetime.now(ZoneInfo(self.get_timezone())).isoformat()
79
+ return f"""<?xml version="1.0" encoding="UTF-8"?>
80
+ <Trias version="{self.trias_version}" xmlns="http://www.vdv.de/trias"
81
+ xmlns:siri="http://www.siri.org.uk/siri">
82
+ <ServiceRequest>
83
+ <siri:RequestTimestamp>{now}</siri:RequestTimestamp>
84
+ <siri:RequestorRef>{self.requestor_ref}</siri:RequestorRef>
85
+ <RequestPayload>
86
+ <StopEventRequest>
87
+ <Location>
88
+ <LocationRef>
89
+ <StopPointRef>{stop_id}</StopPointRef>
90
+ </LocationRef>
91
+ <DepArrTime>{now}</DepArrTime>
92
+ </Location>
93
+ <Params>
94
+ <NumberOfResults>{limit}</NumberOfResults>
95
+ <StopEventType>departure</StopEventType>
96
+ <IncludePreviousCalls>false</IncludePreviousCalls>
97
+ <IncludeOnwardCalls>false</IncludeOnwardCalls>
98
+ <IncludeRealtimeData>true</IncludeRealtimeData>
99
+ </Params>
100
+ </StopEventRequest>
101
+ </RequestPayload>
102
+ </ServiceRequest>
103
+ </Trias>"""
104
+
105
+ def _build_location_request(self, search_term: str) -> str:
106
+ now = datetime.now(ZoneInfo(self.get_timezone())).isoformat()
107
+ return f"""<?xml version="1.0" encoding="UTF-8"?>
108
+ <Trias version="{self.trias_version}" xmlns="http://www.vdv.de/trias"
109
+ xmlns:siri="http://www.siri.org.uk/siri">
110
+ <ServiceRequest>
111
+ <siri:RequestTimestamp>{now}</siri:RequestTimestamp>
112
+ <siri:RequestorRef>{self.requestor_ref}</siri:RequestorRef>
113
+ <RequestPayload>
114
+ <LocationInformationRequest>
115
+ <InitialInput>
116
+ <LocationName>{search_term}</LocationName>
117
+ </InitialInput>
118
+ <Restrictions>
119
+ <Type>stop</Type>
120
+ <NumberOfResults>15</NumberOfResults>
121
+ </Restrictions>
122
+ </LocationInformationRequest>
123
+ </RequestPayload>
124
+ </ServiceRequest>
125
+ </Trias>"""
126
+
127
+ def _extra_headers(self) -> Dict[str, str]:
128
+ return {}
129
+
130
+ async def _post_trias(self, xml_body: str) -> Optional[ET.Element]:
131
+ headers = {"Content-Type": "text/xml; charset=utf-8", **self._extra_headers()}
132
+
133
+ try:
134
+ async with self.session.post(
135
+ self.trias_base_url,
136
+ data=xml_body.encode("utf-8"),
137
+ headers=headers,
138
+ timeout=aiohttp.ClientTimeout(total=15),
139
+ ) as response:
140
+ if response.status == 200:
141
+ text = await response.text()
142
+ return ET.fromstring(text)
143
+ else:
144
+ _LOGGER.warning("%s TRIAS API returned status %s", self.provider_name, response.status)
145
+ except aiohttp.ClientError as e:
146
+ _LOGGER.warning("%s TRIAS API request failed: %s", self.provider_name, e)
147
+ except ET.ParseError as e:
148
+ _LOGGER.warning("%s TRIAS XML parse error: %s", self.provider_name, e)
149
+ except Exception as e:
150
+ _LOGGER.warning("%s TRIAS error: %s", self.provider_name, e)
151
+
152
+ return None
153
+
154
+ async def fetch_departures(
155
+ self,
156
+ station_id: Optional[str],
157
+ place_dm: str,
158
+ name_dm: str,
159
+ departures_limit: int,
160
+ ) -> Optional[Dict[str, Any]]:
161
+ if not station_id:
162
+ _LOGGER.warning("%s provider requires a station_id", self.provider_name)
163
+ return None
164
+
165
+ xml_body = self._build_stop_event_request(station_id, departures_limit)
166
+ root = await self._post_trias(xml_body)
167
+ if root is None:
168
+ return None
169
+
170
+ results = root.findall(".//trias:StopEventResult", NS)
171
+
172
+ if not results:
173
+ results = root.findall(
174
+ ".//trias:ServiceDelivery/trias:DeliveryPayload/trias:StopEventResponse/trias:StopEventResult",
175
+ NS,
176
+ )
177
+
178
+ if not results:
179
+ _LOGGER.debug("%s: No StopEventResult elements found", self.provider_name)
180
+ return {"stopEvents": []}
181
+
182
+ stop_events = []
183
+ for result in results:
184
+ stop_event = _find(result, "trias:StopEvent")
185
+ if stop_event is not None:
186
+ stop_events.append(self._stop_event_to_dict(stop_event))
187
+
188
+ return {"stopEvents": stop_events}
189
+
190
+ def _stop_event_to_dict(self, stop_event: ET.Element) -> Dict[str, Any]:
191
+ call = _find(stop_event, "trias:ThisCall/trias:CallAtStop")
192
+ service = _find(stop_event, "trias:Service")
193
+
194
+ timetabled = _text(call, "trias:ServiceDeparture/trias:TimetabledTime")
195
+ estimated = _text(call, "trias:ServiceDeparture/trias:EstimatedTime")
196
+
197
+ platform_text = _text(call, "trias:PlannedBay/trias:Text")
198
+ estimated_platform = _text(call, "trias:EstimatedBay/trias:Text")
199
+
200
+ line_name = _text(service, "trias:PublishedLineName/trias:Text")
201
+ mode = _text(service, "trias:Mode/trias:PtMode")
202
+ submode = (
203
+ _text(service, "trias:Mode/trias:RailSubmode")
204
+ or _text(service, "trias:Mode/trias:BusSubmode")
205
+ or _text(service, "trias:Mode/trias:TramSubmode")
206
+ or _text(service, "trias:Mode/trias:MetroSubmode")
207
+ )
208
+
209
+ destination = _text(service, "trias:DestinationText/trias:Text")
210
+ if not destination:
211
+ dest_stop = _find(service, "trias:DestinationStopPointRef")
212
+ destination = _text(dest_stop, "trias:StopPointName/trias:Text")
213
+
214
+ operator = _text(service, "trias:OperatorRef")
215
+ is_realtime = bool(estimated)
216
+
217
+ return {
218
+ "timetabledTime": timetabled,
219
+ "estimatedTime": estimated,
220
+ "platform": estimated_platform or platform_text,
221
+ "plannedPlatform": platform_text,
222
+ "lineName": line_name,
223
+ "mode": mode,
224
+ "submode": submode,
225
+ "destination": destination,
226
+ "operator": operator,
227
+ "isRealtime": is_realtime,
228
+ }
229
+
230
+ def parse_departure(
231
+ self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
232
+ ) -> Optional[UnifiedDeparture]:
233
+ try:
234
+ timetabled_str = stop.get("timetabledTime", "")
235
+ estimated_str = stop.get("estimatedTime", "")
236
+
237
+ if not timetabled_str:
238
+ return None
239
+
240
+ planned = _parse_dt(timetabled_str)
241
+ if not planned:
242
+ return None
243
+
244
+ planned_local = planned.astimezone(tz)
245
+
246
+ if estimated_str:
247
+ when = _parse_dt(estimated_str)
248
+ when_local = when.astimezone(tz) if when else planned_local
249
+ else:
250
+ when_local = planned_local
251
+
252
+ delay_seconds = (when_local - planned_local).total_seconds()
253
+ delay_minutes = max(0, int(delay_seconds / 60))
254
+
255
+ mode = stop.get("mode", "")
256
+ mode_mapping = self.get_mode_mapping()
257
+ transport_type = mode_mapping.get(mode, "unknown")
258
+
259
+ platform = stop.get("platform", "")
260
+ planned_platform = stop.get("plannedPlatform", "")
261
+ platform_changed = bool(platform and planned_platform and platform != planned_platform)
262
+
263
+ time_diff = when_local - now
264
+ minutes_until = max(0, int(time_diff.total_seconds() / 60))
265
+
266
+ return UnifiedDeparture(
267
+ line=stop.get("lineName", ""),
268
+ destination=stop.get("destination", "Unknown"),
269
+ departure_time=when_local.strftime("%H:%M"),
270
+ planned_time=planned_local.strftime("%H:%M"),
271
+ delay=delay_minutes,
272
+ platform=platform,
273
+ transportation_type=transport_type,
274
+ is_realtime=stop.get("isRealtime", False),
275
+ minutes_until_departure=minutes_until,
276
+ departure_time_obj=when_local,
277
+ description=None,
278
+ agency=stop.get("operator"),
279
+ notices=None,
280
+ planned_platform=planned_platform if platform_changed else None,
281
+ platform_changed=platform_changed,
282
+ )
283
+ except Exception as e:
284
+ _LOGGER.debug("Error parsing %s TRIAS departure: %s", self.provider_name, e)
285
+ return None
286
+
287
+ async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
288
+ xml_body = self._build_location_request(search_term)
289
+ root = await self._post_trias(xml_body)
290
+ if root is None:
291
+ return []
292
+
293
+ results = root.findall(".//trias:LocationResult", NS)
294
+ if not results:
295
+ results = root.findall(".//trias:LocationInformationResponse/trias:LocationResult", NS)
296
+
297
+ stops = []
298
+ for result in results:
299
+ location = _find(result, "trias:Location")
300
+ if location is None:
301
+ continue
302
+
303
+ stop_point = _find(location, "trias:StopPoint")
304
+ if stop_point is None:
305
+ continue
306
+
307
+ stop_id = _text(stop_point, "trias:StopPointRef")
308
+ name = _text(stop_point, "trias:StopPointName/trias:Text")
309
+ place = _text(location, "trias:LocationName/trias:Text")
310
+
311
+ if not stop_id or not name:
312
+ continue
313
+
314
+ stops.append(
315
+ {
316
+ "id": stop_id,
317
+ "name": name,
318
+ "place": place,
319
+ "area_type": "stop",
320
+ }
321
+ )
322
+
323
+ return stops
@@ -0,0 +1,44 @@
1
+ """VAG Freiburg provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import PROVIDER_VAGFR
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class VAGFRProvider(EFABaseProvider):
10
+ """VAG (Freiburg) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_VAGFR
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "VAG (Freiburg)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://efa.vagfr.de/vagfr3/XML_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://efa.vagfr.de/vagfr3/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
34
+ 1: "train", # S-Bahn
35
+ 4: "tram", # Straßenbahn
36
+ 5: "bus", # Stadtbus
37
+ 6: "bus", # Regionalbus
38
+ 7: "bus", # Schnellbus
39
+ 8: "bus", # Nachtbus
40
+ 13: "train", # Regionalzug (RE/RB)
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,73 @@
1
+ """VBN (Verkehrsverbund Bremen/Niedersachsen) providers."""
2
+
3
+ import aiohttp
4
+ from typing import Dict, Optional
5
+
6
+ from ..const import PROVIDER_VBN_OTP, PROVIDER_VBN_TRIAS
7
+ from .otp_base import OTPBaseProvider
8
+ from .trias_base import TRIASBaseProvider
9
+
10
+
11
+ class VBNOTPProvider(OTPBaseProvider):
12
+ """VBN via OpenTripPlanner REST API (http://gtfsr.vbn.de/api/)."""
13
+
14
+ otp_base_url = "http://gtfsr.vbn.de/api/routers/default"
15
+
16
+ def __init__(
17
+ self,
18
+ session: aiohttp.ClientSession,
19
+ api_key: Optional[str] = None,
20
+ api_key_secondary: Optional[str] = None,
21
+ custom_url: Optional[str] = None,
22
+ ) -> None:
23
+ super().__init__(session, api_key, api_key_secondary)
24
+
25
+ @property
26
+ def provider_id(self) -> str:
27
+ return PROVIDER_VBN_OTP
28
+
29
+ @property
30
+ def provider_name(self) -> str:
31
+ return "VBN OTP (Bremen/Niedersachsen)"
32
+
33
+ @property
34
+ def requires_api_key(self) -> bool:
35
+ return True
36
+
37
+ def _auth_headers(self) -> Dict[str, str]:
38
+ h = super()._auth_headers()
39
+ if self.api_key:
40
+ h["Authorization"] = self.api_key
41
+ return h
42
+
43
+
44
+ class VBNTriasProvider(TRIASBaseProvider):
45
+ """VBN via TRIAS XML API (https://fahrplaner.vbn.de/triasproxy/)."""
46
+
47
+ trias_base_url = "https://fahrplaner.vbn.de/triasproxy/"
48
+
49
+ def __init__(
50
+ self,
51
+ session: aiohttp.ClientSession,
52
+ api_key: Optional[str] = None,
53
+ api_key_secondary: Optional[str] = None,
54
+ custom_url: Optional[str] = None,
55
+ ) -> None:
56
+ super().__init__(session, api_key, api_key_secondary)
57
+
58
+ @property
59
+ def provider_id(self) -> str:
60
+ return PROVIDER_VBN_TRIAS
61
+
62
+ @property
63
+ def provider_name(self) -> str:
64
+ return "VBN TRIAS (Bremen/Niedersachsen)"
65
+
66
+ @property
67
+ def requires_api_key(self) -> bool:
68
+ return True
69
+
70
+ def _extra_headers(self) -> Dict[str, str]:
71
+ if self.api_key:
72
+ return {"Authorization": self.api_key}
73
+ return {}
@@ -0,0 +1,46 @@
1
+ """VGN (Verkehrsverbund Großraum Nürnberg) provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import PROVIDER_VGN
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class VGNProvider(EFABaseProvider):
10
+ """VGN (Nürnberg) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_VGN
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "VGN (Nürnberg)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://efa.vgn.de/vgnExt_oeffi/XML_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://efa.vgn.de/vgnExt_oeffi/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
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
+ 13: "train", # Regionalzug (RE/RB)
43
+ }
44
+
45
+ def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
46
+ return lambda s, est, plan: est != plan if est and plan else False
@@ -0,0 +1,51 @@
1
+ """VRN (Verkehrsverbund Rhein-Neckar) provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import PROVIDER_VRN
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class VRNProvider(EFABaseProvider):
10
+ """VRN (Rhein-Neckar) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_VRN
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "VRN (Rhein-Neckar)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://www.vrn.de/mngvrn/XML_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://www.vrn.de/mngvrn/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)
35
+ 2: "subway", # U-Bahn
36
+ 3: "subway", # U-Bahn variant
37
+ 4: "tram", # Tram/Streetcar
38
+ 5: "bus", # City bus
39
+ 6: "bus", # Regional bus
40
+ 7: "bus", # Express bus
41
+ 8: "bus", # Night bus
42
+ 9: "ferry", # Ferry/Ship
43
+ 10: "taxi", # Taxi
44
+ 11: "bus", # Other/Special transport
45
+ 13: "train", # Regionalzug (RE)
46
+ 15: "train", # InterCity (IC)
47
+ 16: "train", # InterCityExpress (ICE)
48
+ }
49
+
50
+ def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
51
+ return lambda s, est, plan: est != plan if est and plan else False
@@ -0,0 +1,51 @@
1
+ """VRR (Verkehrsverbund Rhein-Ruhr) provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import PROVIDER_VRR
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class VRRProvider(EFABaseProvider):
10
+ """VRR (Verkehrsverbund Rhein-Ruhr) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_VRR
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "VRR (NRW)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://openservice-test.vrr.de/static03/XML_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://openservice-test.vrr.de/static03/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)
35
+ 2: "subway", # U-Bahn
36
+ 3: "subway", # U-Bahn variant
37
+ 4: "tram", # Tram/Streetcar
38
+ 5: "bus", # City bus
39
+ 6: "bus", # Regional bus
40
+ 7: "bus", # Express bus
41
+ 8: "bus", # Night bus
42
+ 9: "ferry", # Ferry/Ship
43
+ 10: "taxi", # Taxi
44
+ 11: "bus", # Other/Special transport
45
+ 13: "train", # Regionalzug (RE)
46
+ 15: "train", # InterCity (IC)
47
+ 16: "train", # InterCityExpress (ICE)
48
+ }
49
+
50
+ def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
51
+ return lambda s, est, plan: "MONITORED" in s.get("realtimeStatus", [])
@@ -0,0 +1,45 @@
1
+ """VVO (Verkehrsverbund Oberelbe) provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import PROVIDER_VVO
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class VVOProvider(EFABaseProvider):
10
+ """VVO (Dresden) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_VVO
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "VVO (Dresden)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://efa.vvo-online.de/VMSSL3/XML_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://efa.vvo-online.de/VMSSL3/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", # Tram/Streetcar
36
+ 5: "bus", # City bus
37
+ 6: "bus", # Regional bus
38
+ 7: "bus", # Express bus
39
+ 8: "bus", # Night bus
40
+ 9: "ferry", # Ferry/Ship
41
+ 13: "train", # Regionalzug (RE)
42
+ }
43
+
44
+ def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
45
+ return lambda s, est, plan: est != plan if est and plan else False
@@ -0,0 +1,48 @@
1
+ """VVS (Verkehrs- und Tarifverbund Stuttgart) provider implementation."""
2
+
3
+ from typing import Any, Callable, Dict, Optional
4
+
5
+ from ..const import PROVIDER_VVS
6
+ from .efa_base import EFABaseProvider
7
+
8
+
9
+ class VVSProvider(EFABaseProvider):
10
+ """VVS (Stuttgart) provider."""
11
+
12
+ @property
13
+ def provider_id(self) -> str:
14
+ return PROVIDER_VVS
15
+
16
+ @property
17
+ def provider_name(self) -> str:
18
+ return "VVS (Stuttgart)"
19
+
20
+ @property
21
+ def dm_base_url(self) -> str:
22
+ return "https://www3.vvs.de/mngvvs/XML_DM_REQUEST"
23
+
24
+ @property
25
+ def sf_base_url(self) -> str:
26
+ return "https://www3.vvs.de/mngvvs/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", # Stadtbahn (SSB)
36
+ 3: "subway", # Stadtbahn 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
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