Journey: add regional-only filter and service notices

- connection_type setting (all/regional/long_distance) restricts EFA
  trip requests via inclMOT_N params, so ICE/IC/EC can be filtered out
  for commutes that only care about regional/local transit.
- Surface leg.infos[].infoLinks titles (real disruption/outage notices
  from EFA) on each journey row, marked with a red warning icon.
This commit is contained in:
Honey committed 2026-07-23 09:51:52 +02:00
1 parent d5757a00df
commit 06b90ffe6c
3 files changed
+62 -6

No files matched your search

+49 -5
View File
@@ -58,6 +58,23 @@ RED = (200, 0, 0)
BLACK = (0, 0, 0)
MUTED = (110, 110, 110)
# EFA's inclMOT_N flags select which transport modes a trip request is
# allowed to use -- N is an index into EFA's internal mode table, not the
# "class" value seen in responses. Long-distance rail (IC/EC/ICE) sits at
# 14-16; everything else usable for a regional/local commute (regional
# trains, S-Bahn, buses, trams, etc.) is the rest. Ported from db_info's
# _build_mot_params()/_build_produktgattungen().
_MOT_LONG_DISTANCE = {f"inclMOT_{i}": "true" for i in (14, 15, 16)}
_MOT_REGIONAL = {f"inclMOT_{i}": "true" for i in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 17, 20)}
def _mot_params(connection_type: str) -> dict:
if connection_type == "regional":
return {"includedMeans": "checkbox", **_MOT_REGIONAL}
if connection_type == "long_distance":
return {"includedMeans": "checkbox", **_MOT_LONG_DISTANCE}
return {}
def _in_bounds(lat, lon, bounds) -> bool:
if bounds is None:
@@ -85,6 +102,7 @@ class JourneyScreen(Screen):
"destination_query": "", "destination_label": "", "destination_lat": None, "destination_lon": None,
"results": 3,
"update_minutes": 2,
"connection_type": "all",
}
def get_poll_interval(self, settings: dict) -> int:
@@ -150,7 +168,8 @@ class JourneyScreen(Screen):
except (TypeError, ValueError):
results = 3
params = self._build_params(o_lat, o_lon, d_lat, d_lon, results)
connection_type = settings.get("connection_type") or "all"
params = self._build_params(o_lat, o_lon, d_lat, d_lon, results, connection_type)
for source in EFA_SOURCES:
if not (_in_bounds(o_lat, o_lon, source["bounds"]) and _in_bounds(d_lat, d_lon, source["bounds"])):
@@ -172,9 +191,9 @@ class JourneyScreen(Screen):
return None
def _build_params(self, o_lat, o_lon, d_lat, d_lon, results) -> dict:
def _build_params(self, o_lat, o_lon, d_lat, d_lon, results, connection_type: str = "all") -> dict:
now = datetime.now().astimezone()
return {
params = {
"outputFormat": "rapidJSON",
"language": "de",
"type_origin": "coord",
@@ -192,6 +211,8 @@ class JourneyScreen(Screen):
"lineRestriction": "400",
"serverInfo": "0",
}
params.update(_mot_params(connection_type))
return params
def _parse_journeys(self, data: dict, results: int):
journeys = []
@@ -211,6 +232,7 @@ class JourneyScreen(Screen):
"line": transportation.get("name") or "?",
"origin": leg.get("origin") or {},
"destination": leg.get("destination") or {},
"infos": leg.get("infos") or [],
})
if not legs:
@@ -228,6 +250,18 @@ class JourneyScreen(Screen):
delay_min = int((dep_estimated - dep_planned).total_seconds() // 60) if dep_estimated else 0
arr_actual = arr_estimated or arr_planned
# Service notices (disruptions, elevator outages, etc.) come as
# leg.infos[].infoLinks[].title -- collect titles across all legs
# of this journey, deduped, since the same notice often repeats
# on every leg it affects.
notice_titles = []
for leg in legs:
for info in leg["infos"]:
for link in info.get("infoLinks") or []:
title = link.get("title") or link.get("subtitle")
if title and title not in notice_titles:
notice_titles.append(title)
# EFA returns UTC timestamps (Z suffix) -- astimezone() with no
# args converts to the system's local timezone before display.
# Without this, times showed 2h behind (CEST is UTC+2), making
@@ -239,6 +273,7 @@ class JourneyScreen(Screen):
"lines": [leg["line"] for leg in legs],
"transfers": len(legs) - 1,
"platform": (first["origin"].get("properties") or {}).get("platformName") or "",
"notice": "; ".join(notice_titles),
})
return journeys or None
@@ -300,8 +335,17 @@ class JourneyScreen(Screen):
detail_parts.append(f"{journey['transfers']}x umsteigen")
if journey["platform"]:
detail_parts.append(f"Gl. {journey['platform']}")
detail_line = truncate_to_width(draw, " · ".join(detail_parts), detail_font, width - 2 * pad)
draw.text((pad, top_line_y + time_font.size * 1.25), detail_line, font=detail_font, fill=MUTED)
if journey["notice"]:
detail_parts.append(journey["notice"])
detail_y = top_line_y + time_font.size * 1.25
detail_x = pad
if journey["notice"]:
draw.text((detail_x, detail_y), "⚠", font=detail_font, fill=RED)
detail_x += text_width(draw, "⚠", detail_font) + scale * 0.015
detail_line = truncate_to_width(draw, " · ".join(detail_parts), detail_font, width - pad - detail_x)
draw.text((detail_x, detail_y), detail_line, font=detail_font, fill=MUTED)
return image
+4 -1
View File
@@ -141,8 +141,11 @@ def create_app(config, screens: dict, scheduler) -> Flask:
except (TypeError, ValueError):
results = 3
update_minutes = max(1, int(request.form.get("update_minutes", 2) or 2))
connection_type = request.form.get("connection_type", "all")
if connection_type not in ("all", "regional", "long_distance"):
connection_type = "all"
updates = {"results": results, "update_minutes": update_minutes}
updates = {"results": results, "update_minutes": update_minutes, "connection_type": connection_type}
journey_screen = screens["journey"]
if origin_query:
+9
View File
@@ -52,6 +52,15 @@
<label for="update_minutes">Update every (minutes)</label>
<input type="number" id="update_minutes" name="update_minutes" min="1" value="{{ settings.update_minutes or 2 }}">
</div>
<div class="field">
<label for="connection_type">Connections</label>
<select id="connection_type" name="connection_type">
<option value="all" {{ "selected" if settings.connection_type == "all" or not settings.connection_type }}>All</option>
<option value="regional" {{ "selected" if settings.connection_type == "regional" }}>Regional only (no ICE/IC/EC)</option>
<option value="long_distance" {{ "selected" if settings.connection_type == "long_distance" }}>Long-distance only (ICE/IC/EC)</option>
</select>
<p class="hint">"Regional only" hides ICE/IC/EC long-distance trains -- just S-Bahn, regional trains, buses, trams etc.</p>
</div>
</div>
<button type="submit">Save</button>