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,272 @@
|
|
|
1
|
+
"""RMV (Rhein-Main-Verkehrsverbund) 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_RMV
|
|
12
|
+
from ..models import UnifiedDeparture
|
|
13
|
+
from .base import BaseProvider
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
API_BASE = "https://www.rmv.de/hapi"
|
|
18
|
+
|
|
19
|
+
PRODUCT_MAPPING = {
|
|
20
|
+
"ICE": "train",
|
|
21
|
+
"IC": "train",
|
|
22
|
+
"EC": "train",
|
|
23
|
+
"RE": "train",
|
|
24
|
+
"RB": "train",
|
|
25
|
+
"S": "train",
|
|
26
|
+
"U": "subway",
|
|
27
|
+
"Tram": "tram",
|
|
28
|
+
"Bus": "bus",
|
|
29
|
+
"AST": "bus",
|
|
30
|
+
"Fäh": "ferry",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _parse_dt(s: str) -> Optional[datetime]:
|
|
35
|
+
try:
|
|
36
|
+
return datetime.fromisoformat(s)
|
|
37
|
+
except (ValueError, TypeError):
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _determine_transport_type(product: Dict[str, Any]) -> str:
|
|
42
|
+
cat_out = product.get("catOut", "").strip()
|
|
43
|
+
cat_code = product.get("catCode")
|
|
44
|
+
|
|
45
|
+
for key, transport_type in PRODUCT_MAPPING.items():
|
|
46
|
+
if cat_out.startswith(key):
|
|
47
|
+
return transport_type
|
|
48
|
+
|
|
49
|
+
if cat_code is not None:
|
|
50
|
+
try:
|
|
51
|
+
code = int(cat_code)
|
|
52
|
+
if code in (1, 2):
|
|
53
|
+
return "train"
|
|
54
|
+
elif code in (4, 8):
|
|
55
|
+
return "train"
|
|
56
|
+
elif code == 16:
|
|
57
|
+
return "train"
|
|
58
|
+
elif code == 32:
|
|
59
|
+
return "bus"
|
|
60
|
+
elif code == 64:
|
|
61
|
+
return "ferry"
|
|
62
|
+
elif code == 128:
|
|
63
|
+
return "subway"
|
|
64
|
+
elif code == 256:
|
|
65
|
+
return "tram"
|
|
66
|
+
except (ValueError, TypeError):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
return "unknown"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RMVProvider(BaseProvider):
|
|
73
|
+
"""RMV (Frankfurt/Rhine-Main) provider using HAFAS REST API."""
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def provider_id(self) -> str:
|
|
77
|
+
return PROVIDER_RMV
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def provider_name(self) -> str:
|
|
81
|
+
return "RMV (Frankfurt)"
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def requires_api_key(self) -> bool:
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
def get_timezone(self) -> str:
|
|
88
|
+
return "Europe/Berlin"
|
|
89
|
+
|
|
90
|
+
async def fetch_departures(
|
|
91
|
+
self,
|
|
92
|
+
station_id: Optional[str],
|
|
93
|
+
place_dm: str,
|
|
94
|
+
name_dm: str,
|
|
95
|
+
departures_limit: int,
|
|
96
|
+
) -> Optional[Dict[str, Any]]:
|
|
97
|
+
if not station_id:
|
|
98
|
+
_LOGGER.warning("RMV provider requires a station_id")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
if not self.api_key:
|
|
102
|
+
_LOGGER.error("RMV provider requires an API key")
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
url = (
|
|
106
|
+
f"{API_BASE}/departureBoard"
|
|
107
|
+
f"?accessId={self.api_key}"
|
|
108
|
+
f"&id={station_id}"
|
|
109
|
+
f"&format=json"
|
|
110
|
+
f"&duration=120"
|
|
111
|
+
f"&maxJourneys={departures_limit}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
async with self.session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as response:
|
|
116
|
+
if response.status == 200:
|
|
117
|
+
data = await response.json()
|
|
118
|
+
if not isinstance(data, dict):
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
if "errorCode" in data:
|
|
122
|
+
_LOGGER.warning("RMV API error: %s", data.get("errorText", "unknown"))
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
departures = data.get("Departure", [])
|
|
126
|
+
if isinstance(departures, dict):
|
|
127
|
+
departures = [departures]
|
|
128
|
+
return {"stopEvents": departures}
|
|
129
|
+
elif response.status == 401:
|
|
130
|
+
_LOGGER.error("RMV API: invalid API key")
|
|
131
|
+
else:
|
|
132
|
+
_LOGGER.warning("RMV API returned status %s", response.status)
|
|
133
|
+
except aiohttp.ClientError as e:
|
|
134
|
+
_LOGGER.warning("RMV API request failed: %s", e)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
_LOGGER.warning("RMV API error: %s", e)
|
|
137
|
+
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def parse_departure(
|
|
141
|
+
self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
|
|
142
|
+
) -> Optional[UnifiedDeparture]:
|
|
143
|
+
try:
|
|
144
|
+
date_str = stop.get("date")
|
|
145
|
+
time_str = stop.get("time")
|
|
146
|
+
rt_date_str = stop.get("rtDate")
|
|
147
|
+
rt_time_str = stop.get("rtTime")
|
|
148
|
+
|
|
149
|
+
if not date_str or not time_str:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
planned_dt_str = f"{date_str}T{time_str}"
|
|
153
|
+
planned = _parse_dt(planned_dt_str)
|
|
154
|
+
if not planned:
|
|
155
|
+
return None
|
|
156
|
+
planned_local = planned.replace(tzinfo=tz) if planned.tzinfo is None else planned.astimezone(tz)
|
|
157
|
+
|
|
158
|
+
if rt_date_str and rt_time_str:
|
|
159
|
+
rt_dt_str = f"{rt_date_str}T{rt_time_str}"
|
|
160
|
+
rt = _parse_dt(rt_dt_str)
|
|
161
|
+
if rt:
|
|
162
|
+
when_local = rt.replace(tzinfo=tz) if rt.tzinfo is None else rt.astimezone(tz)
|
|
163
|
+
is_realtime = True
|
|
164
|
+
else:
|
|
165
|
+
when_local = planned_local
|
|
166
|
+
is_realtime = False
|
|
167
|
+
else:
|
|
168
|
+
when_local = planned_local
|
|
169
|
+
is_realtime = False
|
|
170
|
+
|
|
171
|
+
delay_minutes = int((when_local - planned_local).total_seconds() / 60)
|
|
172
|
+
|
|
173
|
+
product = stop.get("ProductAtStop", stop.get("Product", {}))
|
|
174
|
+
if isinstance(product, list):
|
|
175
|
+
product = product[0] if product else {}
|
|
176
|
+
line_name = product.get("line", product.get("name", ""))
|
|
177
|
+
transport_type = _determine_transport_type(product)
|
|
178
|
+
|
|
179
|
+
direction = stop.get("direction", "Unknown")
|
|
180
|
+
platform = stop.get("track", "")
|
|
181
|
+
|
|
182
|
+
rt_track = stop.get("rtTrack", "")
|
|
183
|
+
planned_track = platform
|
|
184
|
+
platform_changed = bool(rt_track and planned_track and rt_track != planned_track)
|
|
185
|
+
if rt_track:
|
|
186
|
+
platform = rt_track
|
|
187
|
+
|
|
188
|
+
time_diff = when_local - now
|
|
189
|
+
minutes_until = max(0, int(time_diff.total_seconds() / 60))
|
|
190
|
+
|
|
191
|
+
notices = []
|
|
192
|
+
for msg in stop.get("Messages", {}).get("Message", []):
|
|
193
|
+
if isinstance(msg, dict):
|
|
194
|
+
text = msg.get("head", "") or msg.get("text", "")
|
|
195
|
+
if text:
|
|
196
|
+
notices.append(text)
|
|
197
|
+
|
|
198
|
+
operator = product.get("operator", product.get("operatorName", ""))
|
|
199
|
+
|
|
200
|
+
return UnifiedDeparture(
|
|
201
|
+
line=line_name,
|
|
202
|
+
destination=direction,
|
|
203
|
+
departure_time=when_local.strftime("%H:%M"),
|
|
204
|
+
planned_time=planned_local.strftime("%H:%M"),
|
|
205
|
+
delay=delay_minutes,
|
|
206
|
+
platform=platform,
|
|
207
|
+
transportation_type=transport_type,
|
|
208
|
+
is_realtime=is_realtime,
|
|
209
|
+
minutes_until_departure=minutes_until,
|
|
210
|
+
departure_time_obj=when_local,
|
|
211
|
+
description=product.get("catOutL"),
|
|
212
|
+
agency=operator if operator else None,
|
|
213
|
+
notices=notices if notices else None,
|
|
214
|
+
planned_platform=planned_track if platform_changed else None,
|
|
215
|
+
platform_changed=platform_changed,
|
|
216
|
+
)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
_LOGGER.debug("Error parsing RMV departure: %s", e)
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
|
|
222
|
+
if not self.api_key:
|
|
223
|
+
_LOGGER.error("RMV provider requires an API key for stop search")
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
url = (
|
|
227
|
+
f"{API_BASE}/location.name"
|
|
228
|
+
f"?accessId={self.api_key}"
|
|
229
|
+
f"&input={quote(search_term, safe='')}"
|
|
230
|
+
f"&format=json"
|
|
231
|
+
f"&maxNo=15"
|
|
232
|
+
f"&type=S"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
async with self.session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
|
237
|
+
if response.status == 200:
|
|
238
|
+
data = await response.json()
|
|
239
|
+
if not isinstance(data, dict):
|
|
240
|
+
return []
|
|
241
|
+
|
|
242
|
+
stop_locations = data.get("stopLocationOrCoordLocation", [])
|
|
243
|
+
results = []
|
|
244
|
+
|
|
245
|
+
for item in stop_locations:
|
|
246
|
+
if not isinstance(item, dict):
|
|
247
|
+
continue
|
|
248
|
+
loc = item.get("StopLocation", {})
|
|
249
|
+
if not loc:
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
name = loc.get("name", "")
|
|
253
|
+
place = ""
|
|
254
|
+
if "," in name:
|
|
255
|
+
parts = name.split(",", 1)
|
|
256
|
+
place = parts[0].strip()
|
|
257
|
+
|
|
258
|
+
results.append(
|
|
259
|
+
{
|
|
260
|
+
"id": loc.get("extId", loc.get("id", "")),
|
|
261
|
+
"name": name,
|
|
262
|
+
"place": place,
|
|
263
|
+
"area_type": "stop",
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
return results
|
|
267
|
+
else:
|
|
268
|
+
_LOGGER.error("RMV API returned status %s", response.status)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
_LOGGER.error("Error searching RMV stops: %s", e)
|
|
271
|
+
|
|
272
|
+
return []
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""RVV (Regensburger Verkehrsverbund) provider implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from ..const import PROVIDER_RVV
|
|
6
|
+
from .efa_base import EFABaseProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RVVProvider(EFABaseProvider):
|
|
10
|
+
"""RVV (Regensburg) provider."""
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def provider_id(self) -> str:
|
|
14
|
+
return PROVIDER_RVV
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def provider_name(self) -> str:
|
|
18
|
+
return "RVV (Regensburg)"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def dm_base_url(self) -> str:
|
|
22
|
+
return "https://efa.rvv.de/efa/XML_DM_REQUEST"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def sf_base_url(self) -> str:
|
|
26
|
+
return "https://efa.rvv.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 {
|
|
33
|
+
0: "train", # High-speed trains (ICE, IC, EC)
|
|
34
|
+
1: "train", # Regional trains (RE, RB)
|
|
35
|
+
5: "bus", # City bus
|
|
36
|
+
6: "bus", # Regional bus
|
|
37
|
+
7: "bus", # Express bus
|
|
38
|
+
8: "bus", # Night bus
|
|
39
|
+
13: "train", # Regionalzug (RE)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def get_realtime_fn(self) -> Callable[[Dict[str, Any], Optional[str], Optional[str]], bool]:
|
|
43
|
+
return lambda s, est, plan: est != plan if est and plan else False
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""SBB (Swiss Federal Railways) provider implementation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timedelta
|
|
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_SBB
|
|
12
|
+
from ..models import UnifiedDeparture
|
|
13
|
+
from .base import BaseProvider
|
|
14
|
+
|
|
15
|
+
_LOGGER = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
API_BASE = "https://transport.opendata.ch/v1"
|
|
18
|
+
|
|
19
|
+
CATEGORY_MAPPING = {
|
|
20
|
+
"ICE": "train",
|
|
21
|
+
"IC": "train",
|
|
22
|
+
"IR": "train",
|
|
23
|
+
"EC": "train",
|
|
24
|
+
"RE": "train",
|
|
25
|
+
"S": "train",
|
|
26
|
+
"TGV": "train",
|
|
27
|
+
"RJ": "train",
|
|
28
|
+
"T": "tram",
|
|
29
|
+
"B": "bus",
|
|
30
|
+
"NFB": "bus",
|
|
31
|
+
"BUS": "bus",
|
|
32
|
+
"BAT": "ferry",
|
|
33
|
+
"FAE": "ferry",
|
|
34
|
+
"M": "subway",
|
|
35
|
+
"FUN": "train",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_dt(s: str) -> Optional[datetime]:
|
|
40
|
+
try:
|
|
41
|
+
return datetime.fromisoformat(s)
|
|
42
|
+
except (ValueError, TypeError):
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SBBProvider(BaseProvider):
|
|
47
|
+
"""SBB (Swiss Federal Railways) provider."""
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def provider_id(self) -> str:
|
|
51
|
+
return PROVIDER_SBB
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def provider_name(self) -> str:
|
|
55
|
+
return "SBB (Schweiz)"
|
|
56
|
+
|
|
57
|
+
def get_timezone(self) -> str:
|
|
58
|
+
return "Europe/Zurich"
|
|
59
|
+
|
|
60
|
+
async def fetch_departures(
|
|
61
|
+
self,
|
|
62
|
+
station_id: Optional[str],
|
|
63
|
+
place_dm: str,
|
|
64
|
+
name_dm: str,
|
|
65
|
+
departures_limit: int,
|
|
66
|
+
) -> Optional[Dict[str, Any]]:
|
|
67
|
+
if station_id:
|
|
68
|
+
url = f"{API_BASE}/stationboard?id={station_id}&limit={departures_limit}"
|
|
69
|
+
else:
|
|
70
|
+
station_name = f"{name_dm}, {place_dm}" if place_dm else name_dm
|
|
71
|
+
url = f"{API_BASE}/stationboard?station={quote(station_name, safe='')}&limit={departures_limit}"
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
async with self.session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as response:
|
|
75
|
+
if response.status == 200:
|
|
76
|
+
data = await response.json()
|
|
77
|
+
if not isinstance(data, dict):
|
|
78
|
+
return None
|
|
79
|
+
stationboard = data.get("stationboard", [])
|
|
80
|
+
return {"stopEvents": stationboard}
|
|
81
|
+
else:
|
|
82
|
+
_LOGGER.warning("SBB API returned status %s", response.status)
|
|
83
|
+
except aiohttp.ClientError as e:
|
|
84
|
+
_LOGGER.warning("SBB API request failed: %s", e)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
_LOGGER.warning("SBB API error: %s", e)
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def parse_departure(
|
|
91
|
+
self, stop: Dict[str, Any], tz: Union[ZoneInfo, Any], now: datetime
|
|
92
|
+
) -> Optional[UnifiedDeparture]:
|
|
93
|
+
try:
|
|
94
|
+
stop_info = stop.get("stop", {})
|
|
95
|
+
dep_str = stop_info.get("departure")
|
|
96
|
+
if not dep_str:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
dep_dt = _parse_dt(dep_str)
|
|
100
|
+
if not dep_dt:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
dep_local = dep_dt.astimezone(tz)
|
|
104
|
+
delay_min = stop_info.get("delay") or 0
|
|
105
|
+
|
|
106
|
+
planned_local = dep_local - timedelta(minutes=delay_min)
|
|
107
|
+
|
|
108
|
+
category = stop.get("category", "")
|
|
109
|
+
number = stop.get("number", "")
|
|
110
|
+
line = f"{category}{number}"
|
|
111
|
+
|
|
112
|
+
transport_type = CATEGORY_MAPPING.get(category, "unknown")
|
|
113
|
+
destination = stop.get("to", "Unknown")
|
|
114
|
+
platform = stop_info.get("platform", "")
|
|
115
|
+
|
|
116
|
+
time_diff = dep_local - now
|
|
117
|
+
minutes_until = max(0, int(time_diff.total_seconds() / 60))
|
|
118
|
+
|
|
119
|
+
is_realtime = stop_info.get("prognosis", {}).get("departure") is not None
|
|
120
|
+
|
|
121
|
+
prognosis_platform = stop_info.get("prognosis", {}).get("platform")
|
|
122
|
+
platform_changed = bool(prognosis_platform and platform and prognosis_platform != platform)
|
|
123
|
+
planned_platform = platform if platform_changed else None
|
|
124
|
+
if prognosis_platform:
|
|
125
|
+
platform = prognosis_platform
|
|
126
|
+
|
|
127
|
+
return UnifiedDeparture(
|
|
128
|
+
line=line,
|
|
129
|
+
destination=destination,
|
|
130
|
+
departure_time=dep_local.strftime("%H:%M"),
|
|
131
|
+
planned_time=planned_local.strftime("%H:%M"),
|
|
132
|
+
delay=delay_min,
|
|
133
|
+
platform=platform,
|
|
134
|
+
transportation_type=transport_type,
|
|
135
|
+
is_realtime=is_realtime,
|
|
136
|
+
minutes_until_departure=minutes_until,
|
|
137
|
+
departure_time_obj=dep_local,
|
|
138
|
+
description=stop.get("operator", ""),
|
|
139
|
+
notices=None,
|
|
140
|
+
planned_platform=planned_platform,
|
|
141
|
+
platform_changed=platform_changed,
|
|
142
|
+
)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
_LOGGER.debug("Error parsing SBB departure: %s", e)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
async def search_stops(self, search_term: str) -> List[Dict[str, Any]]:
|
|
148
|
+
url = f"{API_BASE}/locations?query={quote(search_term, safe='')}&type=station"
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
async with self.session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
|
152
|
+
if response.status == 200:
|
|
153
|
+
data = await response.json()
|
|
154
|
+
stations = data.get("stations", [])
|
|
155
|
+
results = []
|
|
156
|
+
for station in stations:
|
|
157
|
+
if not isinstance(station, dict) or not station.get("id"):
|
|
158
|
+
continue
|
|
159
|
+
name = station.get("name", "")
|
|
160
|
+
results.append(
|
|
161
|
+
{
|
|
162
|
+
"id": str(station.get("id", "")),
|
|
163
|
+
"name": name,
|
|
164
|
+
"place": "",
|
|
165
|
+
"area_type": "stop",
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
return results
|
|
169
|
+
else:
|
|
170
|
+
_LOGGER.error("SBB API returned status %s", response.status)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
_LOGGER.error("Error searching SBB stops: %s", e)
|
|
173
|
+
|
|
174
|
+
return []
|