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.
- openpublictransport/__init__.py +5 -0
- openpublictransport/const.py +75 -0
- openpublictransport/models.py +95 -0
- openpublictransport/parsers.py +122 -0
- openpublictransport/providers/__init__.py +127 -0
- openpublictransport/providers/avv.py +44 -0
- openpublictransport/providers/base.py +77 -0
- openpublictransport/providers/beg.py +46 -0
- openpublictransport/providers/bsvg.py +44 -0
- openpublictransport/providers/bvg.py +32 -0
- openpublictransport/providers/db.py +39 -0
- openpublictransport/providers/ding.py +44 -0
- openpublictransport/providers/efa_base.py +209 -0
- openpublictransport/providers/fptf_base.py +169 -0
- openpublictransport/providers/gtfsde.py +21 -0
- openpublictransport/providers/hvv.py +40 -0
- openpublictransport/providers/kvv.py +38 -0
- openpublictransport/providers/mvv.py +48 -0
- openpublictransport/providers/nta.py +288 -0
- openpublictransport/providers/nvbw.py +44 -0
- openpublictransport/providers/nwl.py +46 -0
- openpublictransport/providers/oebb.py +182 -0
- openpublictransport/providers/otp.py +378 -0
- openpublictransport/providers/otp_base.py +268 -0
- openpublictransport/providers/otp_custom.py +22 -0
- openpublictransport/providers/rmv.py +272 -0
- openpublictransport/providers/rvv.py +43 -0
- openpublictransport/providers/sbb.py +174 -0
- openpublictransport/providers/trafiklab.py +280 -0
- openpublictransport/providers/transitous.py +198 -0
- openpublictransport/providers/trias_base.py +323 -0
- openpublictransport/providers/vagfr.py +44 -0
- openpublictransport/providers/vbn.py +73 -0
- openpublictransport/providers/vgn.py +46 -0
- openpublictransport/providers/vrn.py +51 -0
- openpublictransport/providers/vrr.py +51 -0
- openpublictransport/providers/vvo.py +45 -0
- openpublictransport/providers/vvs.py +48 -0
- python_openpublictransport-0.1.0.dist-info/METADATA +14 -0
- python_openpublictransport-0.1.0.dist-info/RECORD +42 -0
- python_openpublictransport-0.1.0.dist-info/WHEEL +5 -0
- python_openpublictransport-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Trafiklab (Sweden) provider implementation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
from zoneinfo import ZoneInfo
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
from aiohttp import ClientConnectorError
|
|
12
|
+
|
|
13
|
+
from ..const import API_BASE_URL_TRAFIKLAB, PROVIDER_TRAFIKLAB_SE, TRAFIKLAB_TRANSPORTATION_TYPES
|
|
14
|
+
from ..models import UnifiedDeparture
|
|
15
|
+
from ..parsers import parse_departure_generic
|
|
16
|
+
from .base import BaseProvider
|
|
17
|
+
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TrafiklabProvider(BaseProvider):
|
|
22
|
+
"""Trafiklab (Sweden) provider."""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def provider_id(self) -> str:
|
|
26
|
+
return PROVIDER_TRAFIKLAB_SE
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def provider_name(self) -> str:
|
|
30
|
+
return "Trafiklab (Sweden)"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def requires_api_key(self) -> bool:
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def get_timezone(self) -> str:
|
|
37
|
+
return "Europe/Stockholm"
|
|
38
|
+
|
|
39
|
+
async def fetch_departures(
|
|
40
|
+
self,
|
|
41
|
+
station_id: Optional[str],
|
|
42
|
+
place_dm: str,
|
|
43
|
+
name_dm: str,
|
|
44
|
+
departures_limit: int,
|
|
45
|
+
) -> Optional[Dict[str, Any]]:
|
|
46
|
+
if not self.api_key:
|
|
47
|
+
_LOGGER.error("Trafiklab API key is required")
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
if not station_id:
|
|
51
|
+
_LOGGER.error("Trafiklab requires a station ID")
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
url = f"{API_BASE_URL_TRAFIKLAB}/departures/{station_id}"
|
|
55
|
+
params = {"key": self.api_key}
|
|
56
|
+
|
|
57
|
+
headers = {"User-Agent": "Mozilla/5.0 (compatible; OpenPublicTransport Trafiklab)"}
|
|
58
|
+
|
|
59
|
+
max_retries = 3
|
|
60
|
+
for attempt in range(1, max_retries + 1):
|
|
61
|
+
try:
|
|
62
|
+
async with self.session.get(
|
|
63
|
+
url, params=params, headers=headers, timeout=aiohttp.ClientTimeout(total=10)
|
|
64
|
+
) as response:
|
|
65
|
+
if response.status == 200:
|
|
66
|
+
try:
|
|
67
|
+
json_data = await response.json()
|
|
68
|
+
if not isinstance(json_data, dict):
|
|
69
|
+
_LOGGER.warning("Trafiklab API returned non-dict response: %s", type(json_data))
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
if "departures" not in json_data:
|
|
73
|
+
_LOGGER.debug("Trafiklab API response missing 'departures' field")
|
|
74
|
+
return {"stopEvents": []}
|
|
75
|
+
|
|
76
|
+
departures = json_data.get("departures", [])
|
|
77
|
+
_LOGGER.debug("Trafiklab API returned %d departures", len(departures))
|
|
78
|
+
stop_events = []
|
|
79
|
+
|
|
80
|
+
stockholm_tz = ZoneInfo("Europe/Stockholm")
|
|
81
|
+
now_stockholm = datetime.now(stockholm_tz)
|
|
82
|
+
offset = now_stockholm.strftime("%z")
|
|
83
|
+
offset_formatted = f"{offset[:3]}:{offset[3:]}" # +0100 -> +01:00
|
|
84
|
+
|
|
85
|
+
for dep in departures:
|
|
86
|
+
if not isinstance(dep, dict):
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
scheduled_time = dep.get("scheduled")
|
|
90
|
+
realtime_time = dep.get("realtime")
|
|
91
|
+
route = dep.get("route") or {}
|
|
92
|
+
platform_data = dep.get("scheduled_platform") or dep.get("realtime_platform") or {}
|
|
93
|
+
transport_mode = route.get("transport_mode", "BUS") if route else "BUS"
|
|
94
|
+
|
|
95
|
+
destination_obj = route.get("destination") if route else None
|
|
96
|
+
destination_name = (
|
|
97
|
+
destination_obj.get("name", "Unknown")
|
|
98
|
+
if isinstance(destination_obj, dict)
|
|
99
|
+
else "Unknown"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if scheduled_time and "+" not in scheduled_time and "Z" not in scheduled_time:
|
|
103
|
+
scheduled_time = f"{scheduled_time}{offset_formatted}"
|
|
104
|
+
if realtime_time and "+" not in realtime_time and "Z" not in realtime_time:
|
|
105
|
+
realtime_time = f"{realtime_time}{offset_formatted}"
|
|
106
|
+
|
|
107
|
+
stop_event = {
|
|
108
|
+
"departureTimePlanned": scheduled_time,
|
|
109
|
+
"departureTimeEstimated": realtime_time or scheduled_time,
|
|
110
|
+
"transportation": {
|
|
111
|
+
"number": route.get("designation", "") if route else "",
|
|
112
|
+
"description": (
|
|
113
|
+
(route.get("name") or route.get("direction", "")) if route else ""
|
|
114
|
+
),
|
|
115
|
+
"destination": {"name": destination_name},
|
|
116
|
+
"product": {"class": 0},
|
|
117
|
+
},
|
|
118
|
+
"platform": {"name": platform_data.get("designation", "") if platform_data else ""},
|
|
119
|
+
"realtimeStatus": ["MONITORED"] if dep.get("is_realtime") else [],
|
|
120
|
+
"transportMode": transport_mode,
|
|
121
|
+
}
|
|
122
|
+
stop_events.append(stop_event)
|
|
123
|
+
|
|
124
|
+
return {"stopEvents": stop_events}
|
|
125
|
+
except (ValueError, aiohttp.ContentTypeError) as e:
|
|
126
|
+
_LOGGER.warning("Trafiklab API returned invalid JSON: %s", e)
|
|
127
|
+
return None
|
|
128
|
+
except Exception as e:
|
|
129
|
+
_LOGGER.warning("Trafiklab API JSON parsing failed: %s", e)
|
|
130
|
+
return None
|
|
131
|
+
elif response.status == 404:
|
|
132
|
+
_LOGGER.warning("Trafiklab API endpoint not found (404)")
|
|
133
|
+
return None
|
|
134
|
+
elif response.status == 401:
|
|
135
|
+
_LOGGER.warning("Trafiklab API authentication failed (401) - check API key")
|
|
136
|
+
return None
|
|
137
|
+
elif response.status >= 500:
|
|
138
|
+
_LOGGER.warning(
|
|
139
|
+
"Trafiklab API server error (status %s) on attempt %d/%d",
|
|
140
|
+
response.status,
|
|
141
|
+
attempt,
|
|
142
|
+
max_retries,
|
|
143
|
+
)
|
|
144
|
+
if attempt < max_retries:
|
|
145
|
+
await asyncio.sleep(2**attempt)
|
|
146
|
+
continue
|
|
147
|
+
return None
|
|
148
|
+
else:
|
|
149
|
+
_LOGGER.warning(
|
|
150
|
+
"Trafiklab API returned status %s on attempt %d/%d", response.status, attempt, max_retries
|
|
151
|
+
)
|
|
152
|
+
if attempt < max_retries:
|
|
153
|
+
await asyncio.sleep(2**attempt)
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
except asyncio.TimeoutError:
|
|
157
|
+
_LOGGER.warning("Trafiklab API timeout on attempt %d/%d", attempt, max_retries)
|
|
158
|
+
if attempt < max_retries:
|
|
159
|
+
await asyncio.sleep(2**attempt)
|
|
160
|
+
continue
|
|
161
|
+
except ClientConnectorError as e:
|
|
162
|
+
_LOGGER.warning("Trafiklab API connection error on attempt %d/%d: %s", attempt, max_retries, e)
|
|
163
|
+
if attempt < max_retries:
|
|
164
|
+
await asyncio.sleep(2**attempt)
|
|
165
|
+
continue
|
|
166
|
+
except Exception as e:
|
|
167
|
+
_LOGGER.warning("Attempt %d/%d failed: %s", attempt, max_retries, e)
|
|
168
|
+
if attempt < max_retries:
|
|
169
|
+
await asyncio.sleep(2**attempt)
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def parse_departure(
|
|
175
|
+
self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
|
|
176
|
+
) -> Optional[UnifiedDeparture]:
|
|
177
|
+
transport_mode = stop.get("transportMode", "BUS")
|
|
178
|
+
transport_type = TRAFIKLAB_TRANSPORTATION_TYPES.get(transport_mode, "bus")
|
|
179
|
+
|
|
180
|
+
return parse_departure_generic(
|
|
181
|
+
stop,
|
|
182
|
+
tz,
|
|
183
|
+
now,
|
|
184
|
+
get_transport_type_fn=lambda t: transport_type,
|
|
185
|
+
get_platform_fn=lambda s: (
|
|
186
|
+
s.get("platform", {}).get("name", "")
|
|
187
|
+
if isinstance(s.get("platform"), dict)
|
|
188
|
+
else str(s.get("platform", ""))
|
|
189
|
+
),
|
|
190
|
+
get_realtime_fn=lambda s, est, plan: est != plan if est and plan else False,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
|
|
194
|
+
if not self.api_key:
|
|
195
|
+
_LOGGER.error("Trafiklab API key is required for stop search")
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
encoded_search = quote(search_term, safe="")
|
|
199
|
+
url = f"{API_BASE_URL_TRAFIKLAB}/stops/name/{encoded_search}"
|
|
200
|
+
params = {"key": self.api_key}
|
|
201
|
+
|
|
202
|
+
max_retries = 3
|
|
203
|
+
for attempt in range(1, max_retries + 1):
|
|
204
|
+
try:
|
|
205
|
+
async with self.session.get(url, params=params, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
|
206
|
+
if response.status == 200:
|
|
207
|
+
try:
|
|
208
|
+
data = await response.json()
|
|
209
|
+
except (ValueError, aiohttp.ContentTypeError) as e:
|
|
210
|
+
_LOGGER.error("Invalid JSON response from Trafiklab API: %s", e)
|
|
211
|
+
if attempt < max_retries:
|
|
212
|
+
await asyncio.sleep(2**attempt)
|
|
213
|
+
continue
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
if not isinstance(data, dict):
|
|
217
|
+
_LOGGER.error("Trafiklab API returned non-dict response: %s", type(data))
|
|
218
|
+
if attempt < max_retries:
|
|
219
|
+
await asyncio.sleep(2**attempt)
|
|
220
|
+
continue
|
|
221
|
+
return []
|
|
222
|
+
|
|
223
|
+
stop_groups = data.get("stop_groups", [])
|
|
224
|
+
results = []
|
|
225
|
+
|
|
226
|
+
for stop_group in stop_groups:
|
|
227
|
+
if not isinstance(stop_group, dict):
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
stops = stop_group.get("stops", [])
|
|
231
|
+
place = None
|
|
232
|
+
if stops and isinstance(stops[0], dict):
|
|
233
|
+
stop_name = stop_group.get("name", "")
|
|
234
|
+
place = stop_name.split(",")[-1].strip() if "," in stop_name else None
|
|
235
|
+
|
|
236
|
+
result = {
|
|
237
|
+
"id": stop_group.get("id", ""),
|
|
238
|
+
"name": stop_group.get("name", ""),
|
|
239
|
+
"place": place or "",
|
|
240
|
+
"area_type": stop_group.get("area_type", ""),
|
|
241
|
+
"transport_modes": stop_group.get("transport_modes", []),
|
|
242
|
+
}
|
|
243
|
+
results.append(result)
|
|
244
|
+
|
|
245
|
+
return results
|
|
246
|
+
elif response.status == 401:
|
|
247
|
+
_LOGGER.error("Trafiklab API authentication failed (401) - check API key")
|
|
248
|
+
return []
|
|
249
|
+
elif response.status == 404:
|
|
250
|
+
_LOGGER.warning("Trafiklab API endpoint not found (404)")
|
|
251
|
+
return []
|
|
252
|
+
elif response.status >= 500:
|
|
253
|
+
_LOGGER.warning(
|
|
254
|
+
"Trafiklab API server error (status %s) on attempt %d/%d",
|
|
255
|
+
response.status,
|
|
256
|
+
attempt,
|
|
257
|
+
max_retries,
|
|
258
|
+
)
|
|
259
|
+
if attempt < max_retries:
|
|
260
|
+
await asyncio.sleep(2**attempt)
|
|
261
|
+
continue
|
|
262
|
+
else:
|
|
263
|
+
_LOGGER.warning(
|
|
264
|
+
"Trafiklab API returned status %s on attempt %d/%d", response.status, attempt, max_retries
|
|
265
|
+
)
|
|
266
|
+
if attempt < max_retries:
|
|
267
|
+
await asyncio.sleep(2**attempt)
|
|
268
|
+
continue
|
|
269
|
+
except asyncio.TimeoutError:
|
|
270
|
+
_LOGGER.error("Trafiklab API request timeout")
|
|
271
|
+
if attempt < max_retries:
|
|
272
|
+
await asyncio.sleep(2**attempt)
|
|
273
|
+
continue
|
|
274
|
+
except Exception as e:
|
|
275
|
+
_LOGGER.error("Error searching stops: %s", e, exc_info=True)
|
|
276
|
+
if attempt < max_retries:
|
|
277
|
+
await asyncio.sleep(2**attempt)
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
return []
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Transitous (MOTIS2) provider implementation."""
|
|
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 ..const import PROVIDER_TRANSITOUS
|
|
12
|
+
from ..models import UnifiedDeparture
|
|
13
|
+
from .base import BaseProvider
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
API_BASE = "https://api.transitous.org/api"
|
|
18
|
+
USER_AGENT = "OpenPublicTransport/1.0 (github.com/NerdySoftPaw/openpublictransport)"
|
|
19
|
+
|
|
20
|
+
MODE_MAPPING = {
|
|
21
|
+
"HIGHSPEED_RAIL": "train",
|
|
22
|
+
"LONG_DISTANCE": "train",
|
|
23
|
+
"COACH": "bus",
|
|
24
|
+
"NIGHT_RAIL": "train",
|
|
25
|
+
"REGIONAL_FAST_RAIL": "train",
|
|
26
|
+
"REGIONAL_RAIL": "train",
|
|
27
|
+
"SUBURBAN": "train",
|
|
28
|
+
"SUBWAY": "subway",
|
|
29
|
+
"TRAM": "tram",
|
|
30
|
+
"BUS": "bus",
|
|
31
|
+
"FERRY": "ferry",
|
|
32
|
+
"ODM": "bus",
|
|
33
|
+
"FLEXIBLE": "bus",
|
|
34
|
+
"FUNICULAR": "train",
|
|
35
|
+
"GONDOLA": "train",
|
|
36
|
+
"CABLE_CAR": "train",
|
|
37
|
+
"MONORAIL": "train",
|
|
38
|
+
"TROLLEYBUS": "bus",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_dt(s: str) -> Optional[datetime]:
|
|
43
|
+
try:
|
|
44
|
+
return datetime.fromisoformat(s)
|
|
45
|
+
except (ValueError, TypeError):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TransitousProvider(BaseProvider):
|
|
50
|
+
"""Transitous provider — worldwide public transport via MOTIS2."""
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def provider_id(self) -> str:
|
|
54
|
+
return PROVIDER_TRANSITOUS
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def provider_name(self) -> str:
|
|
58
|
+
return "Transitous (Weltweit)"
|
|
59
|
+
|
|
60
|
+
def get_timezone(self) -> str:
|
|
61
|
+
return "Europe/Berlin"
|
|
62
|
+
|
|
63
|
+
async def fetch_departures(
|
|
64
|
+
self,
|
|
65
|
+
station_id: Optional[str],
|
|
66
|
+
place_dm: str,
|
|
67
|
+
name_dm: str,
|
|
68
|
+
departures_limit: int,
|
|
69
|
+
) -> Optional[Dict[str, Any]]:
|
|
70
|
+
if not station_id:
|
|
71
|
+
_LOGGER.warning("Transitous provider requires a station_id")
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
url = f"{API_BASE}/v5/stoptimes?stopId={quote(station_id, safe='')}&n={departures_limit}"
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
async with self.session.get(
|
|
78
|
+
url, headers={"User-Agent": USER_AGENT}, timeout=aiohttp.ClientTimeout(total=15)
|
|
79
|
+
) as response:
|
|
80
|
+
if response.status == 200:
|
|
81
|
+
data = await response.json()
|
|
82
|
+
if not isinstance(data, dict):
|
|
83
|
+
return None
|
|
84
|
+
return {"stopEvents": data.get("stopTimes", [])}
|
|
85
|
+
else:
|
|
86
|
+
_LOGGER.warning("Transitous API returned status %s", response.status)
|
|
87
|
+
except aiohttp.ClientError as e:
|
|
88
|
+
_LOGGER.warning("Transitous API request failed: %s", e)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
_LOGGER.warning("Transitous API error: %s", e)
|
|
91
|
+
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def parse_departure(
|
|
95
|
+
self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
|
|
96
|
+
) -> Optional[UnifiedDeparture]:
|
|
97
|
+
try:
|
|
98
|
+
place = stop.get("place", {})
|
|
99
|
+
dep_str = place.get("departure") or place.get("scheduledDeparture")
|
|
100
|
+
sched_str = place.get("scheduledDeparture")
|
|
101
|
+
|
|
102
|
+
if not dep_str:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
dep_dt = _parse_dt(dep_str)
|
|
106
|
+
sched_dt = _parse_dt(sched_str) if sched_str else dep_dt
|
|
107
|
+
if not dep_dt or not sched_dt:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
stop_tz_str = place.get("tz")
|
|
111
|
+
if stop_tz_str:
|
|
112
|
+
try:
|
|
113
|
+
stop_tz = ZoneInfo(stop_tz_str)
|
|
114
|
+
except (KeyError, ValueError):
|
|
115
|
+
stop_tz = tz
|
|
116
|
+
else:
|
|
117
|
+
stop_tz = tz
|
|
118
|
+
|
|
119
|
+
dep_local = dep_dt.astimezone(stop_tz)
|
|
120
|
+
sched_local = sched_dt.astimezone(stop_tz)
|
|
121
|
+
|
|
122
|
+
delay_minutes = int((dep_local - sched_local).total_seconds() / 60)
|
|
123
|
+
|
|
124
|
+
mode = stop.get("mode", "")
|
|
125
|
+
transport_type = MODE_MAPPING.get(mode, "unknown")
|
|
126
|
+
|
|
127
|
+
line = stop.get("routeShortName") or stop.get("displayName", "")
|
|
128
|
+
destination = stop.get("headsign", "Unknown")
|
|
129
|
+
|
|
130
|
+
track = place.get("track", "")
|
|
131
|
+
sched_track = place.get("scheduledTrack", "")
|
|
132
|
+
platform_changed = bool(track and sched_track and track != sched_track)
|
|
133
|
+
|
|
134
|
+
time_diff = dep_local - now
|
|
135
|
+
minutes_until = max(0, int(time_diff.total_seconds() / 60))
|
|
136
|
+
|
|
137
|
+
is_realtime = stop.get("realTime", False)
|
|
138
|
+
is_cancelled = stop.get("cancelled", False) or stop.get("tripCancelled", False)
|
|
139
|
+
|
|
140
|
+
notices = []
|
|
141
|
+
if is_cancelled:
|
|
142
|
+
notices.append("Fällt aus / Cancelled")
|
|
143
|
+
|
|
144
|
+
agency = stop.get("agencyName", "")
|
|
145
|
+
|
|
146
|
+
return UnifiedDeparture(
|
|
147
|
+
line=line,
|
|
148
|
+
destination=destination,
|
|
149
|
+
departure_time=dep_local.strftime("%H:%M"),
|
|
150
|
+
planned_time=sched_local.strftime("%H:%M"),
|
|
151
|
+
delay=delay_minutes,
|
|
152
|
+
platform=track,
|
|
153
|
+
transportation_type=transport_type,
|
|
154
|
+
is_realtime=is_realtime,
|
|
155
|
+
minutes_until_departure=minutes_until,
|
|
156
|
+
departure_time_obj=dep_local,
|
|
157
|
+
description=stop.get("routeLongName"),
|
|
158
|
+
agency=agency if agency else None,
|
|
159
|
+
notices=notices if notices else None,
|
|
160
|
+
planned_platform=sched_track if platform_changed else None,
|
|
161
|
+
platform_changed=platform_changed,
|
|
162
|
+
)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
_LOGGER.debug("Error parsing Transitous departure: %s", e)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
|
|
168
|
+
url = f"{API_BASE}/v1/geocode?text={quote(search_term, safe='')}&type=STOP"
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
async with self.session.get(
|
|
172
|
+
url, headers={"User-Agent": USER_AGENT}, timeout=aiohttp.ClientTimeout(total=10)
|
|
173
|
+
) as response:
|
|
174
|
+
if response.status == 200:
|
|
175
|
+
data = await response.json()
|
|
176
|
+
if not isinstance(data, list):
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
results = []
|
|
180
|
+
for location in data:
|
|
181
|
+
if not isinstance(location, dict):
|
|
182
|
+
continue
|
|
183
|
+
name = location.get("name", "")
|
|
184
|
+
results.append(
|
|
185
|
+
{
|
|
186
|
+
"id": location.get("id", ""),
|
|
187
|
+
"name": name,
|
|
188
|
+
"place": "",
|
|
189
|
+
"area_type": "stop",
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
return results
|
|
193
|
+
else:
|
|
194
|
+
_LOGGER.error("Transitous API returned status %s", response.status)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
_LOGGER.error("Error searching Transitous stops: %s", e)
|
|
197
|
+
|
|
198
|
+
return []
|