updated functionality, --light (terminal mode) or Textual mode (as default)

This commit is contained in:
2025-08-14 16:34:05 +02:00
parent dc4e8c6ffa
commit 0d539cd470

View File

@@ -39,14 +39,18 @@ import argparse
import requests import requests
import time import time
import json import json
import os
import sys
from datetime import datetime from datetime import datetime
from rich.console import Console, Group from rich.console import Console, Group
from rich.table import Table from rich.table import Table
from rich.live import Live from rich.live import Live
from rich.align import Align from rich.align import Align
# Neu für Webversion
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.widgets import Header, Footer, Input, DataTable, Static
from textual.color import Color
# %% Main # %% Main
# Option 1: Manuell API Key laden: # Option 1: Manuell API Key laden:
#API_KEY = "" #"DEIN_API_KEY_HIER" #API_KEY = "" #"DEIN_API_KEY_HIER"
@@ -101,20 +105,7 @@ line_colors = {
console = Console() console = Console()
def fetch_departures(start_id, max_journeys=12):
def fetch_departures(start_id, max_journeys=5, debug=False):
"""
Ruft Abfahrtsdaten für eine gegebene RMV-Haltestellen-ID von der RMV-HAFAS-API ab.
Args:
starrt_id (str): Die Haltestellen-ID (z. B. '3001605' für Buchrainplatz).
max_journeys (int, optional): Maximale Anzahl zurückzugebender Fahrten. Default: 5.
debug (bool, optional): Wenn True, wird die vollständige API-JSON-Ausgabe angezeigt.
Returns:
list[dict]: Liste der Abfahrten mit Feldern wie Linie, Ziel, Datum und Uhrzeit.
"""
params = { params = {
"id": start_id, "id": start_id,
"format": "json", "format": "json",
@@ -125,10 +116,6 @@ def fetch_departures(start_id, max_journeys=5, debug=False):
resp.raise_for_status() resp.raise_for_status()
data = resp.json() data = resp.json()
if debug:
console.print("[yellow]RAW API Response:[/yellow]")
console.print_json(json.dumps(data, indent=2))
departures = [] departures = []
if "DepartureBoard" in data and isinstance(data["DepartureBoard"], dict): if "DepartureBoard" in data and isinstance(data["DepartureBoard"], dict):
dep = data["DepartureBoard"].get("Departure", []) dep = data["DepartureBoard"].get("Departure", [])
@@ -138,47 +125,21 @@ def fetch_departures(start_id, max_journeys=5, debug=False):
return departures return departures
def build_table(departures): def build_table_data(departures):
""" """Erzeugt Datenlisten für West- und Ost-Richtung."""
Baut zwei getrennte Tabellen (West- und Ost-Richtung) mit den nächsten Abfahrten.
Die Richtung wird anhand von Schlüsselwörtern im Ziel bestimmt.
Duplikate werden gefiltert, und die Abfahrten werden nach Zeit sortiert.
Zusätzlich werden Linien mit definierten Farben dargestellt und
die Anzahl der angezeigten Einträge pro Richtung begrenzt.
Args:
departures (list[dict]): Liste von Abfahrtsdatensätzen aus der RMV-API.
Returns:
rich.console.Group: Zwei ausgerichtete Rich-Tabellen, eine für jede Richtung.
"""
now = datetime.now().strftime("%H:%M:%S") now = datetime.now().strftime("%H:%M:%S")
table_west = Table(title=f"← Richtung Frankfurt (Westen) {now}")
table_west.add_column("Linie", justify="center", style="cyan", no_wrap=True)
table_west.add_column("Ziel", justify="left", style="magenta")
table_west.add_column("In", justify="right", style="green")
table_east = Table(title="→ Richtung Oberrad / Offenbach (Osten)")
table_east.add_column("Linie", justify="center", style="cyan", no_wrap=True)
table_east.add_column("Ziel", justify="left", style="magenta")
table_east.add_column("In", justify="right", style="green")
west_keywords = ["Frankfurt", "Ginnheim", "Lokalbahnhof", "Haardtwaldplatz"] west_keywords = ["Frankfurt", "Ginnheim", "Lokalbahnhof", "Haardtwaldplatz"]
east_keywords = ["Oberrad", "Offenbach", "Stadtgrenze", "Louisa", "Hugo-Junkers", "Balduinstraße"] east_keywords = ["Oberrad", "Offenbach", "Stadtgrenze", "Louisa", "Hugo-Junkers", "Balduinstraße"]
excluded_lines = {"81", "M36"}
excluded_lines = {"81", "M36"} # zu excludierende Buslinien
seen = set() seen = set()
westbound = [] westbound, eastbound = [], []
eastbound = []
for dep in departures: for dep in departures:
line = dep.get("Product", [{}])[0].get("line", "?") if isinstance(dep.get("Product"), list) else dep.get("Product", {}).get("line", "?") line = dep.get("Product", [{}])[0].get("line", "?") if isinstance(dep.get("Product"), list) else dep.get("Product", {}).get("line", "?")
if line in excluded_lines: if line in excluded_lines:
continue continue
dest = dep.get("direction", "").strip() dest = dep.get("direction", "").strip()
if not dest: if not dest:
continue continue
@@ -202,53 +163,128 @@ def build_table(departures):
seen.add(key) seen.add(key)
if any(k in dest for k in west_keywords): if any(k in dest for k in west_keywords):
westbound.append((dep_time, line, dest, mins_str)) westbound.append((line, dest, mins_str))
elif any(k in dest for k in east_keywords): elif any(k in dest for k in east_keywords):
eastbound.append((dep_time, line, dest, mins_str)) eastbound.append((line, dest, mins_str))
# Sortieren westbound = sorted(westbound, key=lambda x: x[2])[:MAX_PER_DIRECTION]
westbound.sort(key=lambda x: x[0]) eastbound = sorted(eastbound, key=lambda x: x[2])[:MAX_PER_DIRECTION]
eastbound.sort(key=lambda x: x[0])
# Begrenzen auf MAX_PER_DIRECTION return now, westbound, eastbound
westbound = westbound[:MAX_PER_DIRECTION]
eastbound = eastbound[:MAX_PER_DIRECTION]
# Tabellen füllen (Linien einfärben)
for _, line, dest, mins_str in westbound:
line_style = f"[{line_colors[line]}]{line}[/]" if line in line_colors else line
table_west.add_row(line_style, dest, mins_str)
for _, line, dest, mins_str in eastbound:
line_style = f"[{line_colors[line]}]{line}[/]" if line in line_colors else line
table_east.add_row(line_style, dest, mins_str)
return Group(Align.center(table_west), Align.center(table_east))
# -------------------
def main(poll_interval=30, debug=False): # Terminalmodus
""" # -------------------
Hauptschleife: Holt regelmäßig Abfahrtsdaten und aktualisiert die Anzeige. def run_terminal_mode():
Args:
poll_interval (int, optional): Abfrageintervall in Sekunden. Default: 30.
debug (bool, optional): Wenn True, wird die API-JSON-Ausgabe angezeigt.
"""
with Live(console=console, refresh_per_second=1) as live: with Live(console=console, refresh_per_second=1) as live:
while True: while True:
try: try:
departures = fetch_departures(START_ID, debug=debug) departures = fetch_departures(START_ID)
table = build_table(departures) now, west, east = build_table_data(departures)
live.update(Align.center(table))
table_west = Table(title=f"← Richtung Frankfurt (Westen) {now}")
table_west.add_column("Linie", justify="center", style="cyan", no_wrap=True)
table_west.add_column("Ziel", justify="left", style="magenta")
table_west.add_column("In", justify="right", style="green")
for line, dest, mins in west:
style = f"[{line_colors[line]}]{line}[/]" if line in line_colors else line
table_west.add_row(style, dest, mins)
table_east = Table(title="→ Richtung Oberrad / Offenbach (Osten)")
table_east.add_column("Linie", justify="center", style="cyan", no_wrap=True)
table_east.add_column("Ziel", justify="left", style="magenta")
table_east.add_column("In", justify="right", style="green")
for line, dest, mins in east:
style = f"[{line_colors[line]}]{line}[/]" if line in line_colors else line
table_east.add_row(style, dest, mins)
live.update(Align.center(Group(Align.center(table_west), Align.center(table_east))))
except Exception as e: except Exception as e:
console.print(f"[red]Fehler bei API-Aufruf:[/red] {e}") console.print(f"[red]Fehler bei API-Aufruf:[/red] {e}")
time.sleep(poll_interval) time.sleep(30)
# -------------------
# Webmodus mit Textual
# -------------------
class RMVApp(App):
CSS_PATH = None
CSS = """
Screen {
align: center middle;
}
#input_id {
width: 40;
margin: 1;
}
"""
BINDINGS = [("q", "quit", "Beenden")]
def __init__(self):
super().__init__()
self.current_id = START_ID
self.table_west = DataTable()
self.table_east = DataTable()
def compose(self) -> ComposeResult:
yield Header()
yield Input(
value=self.current_id,
placeholder="START_ID eingeben und Enter drücken",
id="input_id"
)
yield Container(
Static("← Richtung Frankfurt (Westen)"),
self.table_west,
Static(""), # Leerzeile als Abstand
Static("→ Richtung Oberrad / Offenbach (Osten)"),
self.table_east
)
yield Footer()
async def on_mount(self) -> None:
# Spaltenüberschriften einmalig setzen
self.table_west.add_columns("Linie", "Ziel", "In")
self.table_east.add_columns("Linie", "Ziel", "In")
await self.refresh_tables()
self.set_interval(30, self.refresh_tables)
async def on_input_submitted(self, event: Input.Submitted) -> None:
"""START_ID ändern, wenn Enter gedrückt wird"""
self.current_id = event.value.strip()
await self.refresh_tables()
async def refresh_tables(self) -> None:
try:
departures = fetch_departures(self.current_id)
_, west, east = build_table_data(departures)
self.update_table(self.table_west, west)
self.update_table(self.table_east, east)
except Exception as e:
self.console.print(f"[red]Fehler:[/red] {e}")
def update_table(self, table: DataTable, rows):
"""Befüllt eine DataTable mit neuen Daten und Farbcodes"""
table.clear() # Nur Zeilen löschen, Spalten bleiben
for line, dest, mins in rows:
color_hex = line_colors.get(line)
if color_hex:
table.add_row(f"[{color_hex}]{line}[/]", dest, mins)
else:
table.add_row(line, dest, mins)
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="RMV Live-Abfahrtstafel für Buchrainplatz") parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true", help="Zeige API-JSON-Ausgabe zur Fehlersuche") parser.add_argument("--light", action="store_true", help="Terminalmodus")
args = parser.parse_args() args = parser.parse_args()
main(debug=args.debug) if args.light:
run_terminal_mode()
else:
RMVApp().run()