Upload files to "/"
Initial file upload.
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
apikey.txt
|
||||
.env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
254
RMV_Digital_Board_light.py
Normal file
254
RMV_Digital_Board_light.py
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# Autor: Marcel Weschke - 2025
|
||||
|
||||
# Linienfahrpläne-Quelle:
|
||||
#
|
||||
# Linie 15: https://www.rmv.de/c/fileadmin/import/timetable/traffiQ_2022_AHF_Linie_15_ab_2021_12_12.pdf
|
||||
# Linie 16: https://www.rmv.de/c/fileadmin/import/timetable/traffiQ_2022_Buch_Linie_16_ab_2021_12_12.pdf
|
||||
#
|
||||
|
||||
# =============================================================================
|
||||
# RMV Live-Abfahrtstafel für Buchrainplatz
|
||||
#
|
||||
# Beispiel-Programmausgabe im Terminal:
|
||||
#
|
||||
# ➜ Desktop python RMV_DigiAnzeige_Buchreinplatz.py
|
||||
#
|
||||
# Buchrainplatz – Live-Abfahrtstafel – 11:57:10
|
||||
#
|
||||
# ← Richtung Frankfurt
|
||||
# (Westen)
|
||||
# ┏━━━━━━━┳━━━━━━┳━━━━┓
|
||||
# ┃ Linie ┃ Ziel ┃ In ┃
|
||||
# ┡━━━━━━━╇━━━━━━╇━━━━┩
|
||||
# └───────┴──────┴────┘
|
||||
#
|
||||
# → Richtung Oberrad /
|
||||
# Offenbach (Osten)
|
||||
# ┏━━━━━━━┳━━━━━━┳━━━━┓
|
||||
# ┃ Linie ┃ Ziel ┃ In ┃
|
||||
# ┡━━━━━━━╇━━━━━━╇━━━━┩
|
||||
# └───────┴──────┴────┘
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# %% Load libraries
|
||||
import argparse
|
||||
import requests
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from rich.console import Console, Group
|
||||
from rich.table import Table
|
||||
from rich.live import Live
|
||||
from rich.align import Align
|
||||
|
||||
# %% Main
|
||||
# Option 1: Manuell API Key laden:
|
||||
#API_KEY = "" #"DEIN_API_KEY_HIER"
|
||||
|
||||
# Option 2: Automatisch API Key aus einer Textdatei laden:
|
||||
try:
|
||||
with open("apikey.txt", "r", encoding="utf-8") as f:
|
||||
API_KEY = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError("Fehler: Datei 'apikey.txt' nicht gefunden. Bitte API-Key in dieser Datei hinterlegen.")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Fehler beim Laden des API-Key: {e}")
|
||||
|
||||
|
||||
START_ID = "3001605" # Buchrainplatz
|
||||
#START_ID = "3000906" # Lokalbahnhof/Textorstraße
|
||||
MAX_PER_DIRECTION = 12
|
||||
URL = "https://www.rmv.de/hapi/departureBoard"
|
||||
|
||||
# Linienfarben
|
||||
# Colorcode-Quelle: https://commons.wikimedia.org/wiki/Template:Frankfurt_transit_icons
|
||||
line_colors = {
|
||||
"S1": "#0096da", # S-Bahn-lines
|
||||
"S2": "#ee161f",
|
||||
"S3": "#00aa96",
|
||||
"S4": "#fac800",
|
||||
"S5": "#965b33",
|
||||
"S6": "#f27718",
|
||||
"S7": "#1b533d",
|
||||
"S8": "#8dc63c",
|
||||
"S9": "#91208f",
|
||||
"11": "#fbbd02", # Tram-lines
|
||||
"12": "#c83710",
|
||||
"14": "#f14517",
|
||||
"16": "#f4730e",
|
||||
"15": "#fbbd02",
|
||||
"17": "#f14517",
|
||||
"18": "#f89d06",
|
||||
"19": "#ef3200",
|
||||
"20": "#f79700",
|
||||
"21": "#f89d06",
|
||||
"U1": "#a50010", # U-Bahn-lines
|
||||
"U2": "#00a850",
|
||||
"U3": "#333f8d",
|
||||
"U4": "#e12d82",
|
||||
"U5": "#015c1b",
|
||||
"U6": "#007ec6",
|
||||
"U7": "#df9100",
|
||||
"U8": "#c066a3",
|
||||
"U9": "#ffd700"
|
||||
}
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
|
||||
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 = {
|
||||
"id": start_id,
|
||||
"format": "json",
|
||||
"accessId": API_KEY,
|
||||
"limit": max_journeys
|
||||
}
|
||||
resp = requests.get(URL, params=params, timeout=10)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if debug:
|
||||
console.print("[yellow]RAW API Response:[/yellow]")
|
||||
console.print_json(json.dumps(data, indent=2))
|
||||
|
||||
departures = []
|
||||
if "DepartureBoard" in data and isinstance(data["DepartureBoard"], dict):
|
||||
dep = data["DepartureBoard"].get("Departure", [])
|
||||
departures = dep if isinstance(dep, list) else [dep]
|
||||
elif "Departure" in data:
|
||||
departures = data["Departure"] if isinstance(data["Departure"], list) else [data["Departure"]]
|
||||
return departures
|
||||
|
||||
|
||||
def build_table(departures):
|
||||
"""
|
||||
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")
|
||||
|
||||
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"]
|
||||
east_keywords = ["Oberrad", "Offenbach", "Stadtgrenze", "Louisa", "Hugo-Junkers", "Balduinstraße"]
|
||||
|
||||
excluded_lines = {"81", "M36"} # zu excludierende Buslinien
|
||||
|
||||
seen = set()
|
||||
westbound = []
|
||||
eastbound = []
|
||||
|
||||
for dep in departures:
|
||||
line = dep.get("Product", [{}])[0].get("line", "?") if isinstance(dep.get("Product"), list) else dep.get("Product", {}).get("line", "?")
|
||||
if line in excluded_lines:
|
||||
continue
|
||||
|
||||
dest = dep.get("direction", "").strip()
|
||||
if not dest:
|
||||
continue
|
||||
|
||||
date = dep.get("rtDate") or dep.get("date")
|
||||
time_ = dep.get("rtTime") or dep.get("time")
|
||||
if not date or not time_:
|
||||
continue
|
||||
|
||||
try:
|
||||
dep_time = datetime.fromisoformat(f"{date}T{time_}")
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
mins = int((dep_time - datetime.now()).total_seconds() / 60)
|
||||
mins_str = f"{mins} Min" if mins >= 0 else "Jetzt"
|
||||
|
||||
key = (line, dest, dep_time)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
|
||||
if any(k in dest for k in west_keywords):
|
||||
westbound.append((dep_time, line, dest, mins_str))
|
||||
elif any(k in dest for k in east_keywords):
|
||||
eastbound.append((dep_time, line, dest, mins_str))
|
||||
|
||||
# Sortieren
|
||||
westbound.sort(key=lambda x: x[0])
|
||||
eastbound.sort(key=lambda x: x[0])
|
||||
|
||||
# Begrenzen auf MAX_PER_DIRECTION
|
||||
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):
|
||||
"""
|
||||
Hauptschleife: Holt regelmäßig Abfahrtsdaten und aktualisiert die Anzeige.
|
||||
|
||||
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:
|
||||
while True:
|
||||
try:
|
||||
departures = fetch_departures(START_ID, debug=debug)
|
||||
table = build_table(departures)
|
||||
live.update(Align.center(table))
|
||||
except Exception as e:
|
||||
console.print(f"[red]Fehler bei API-Aufruf:[/red] {e}")
|
||||
time.sleep(poll_interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="RMV Live-Abfahrtstafel für Buchrainplatz")
|
||||
parser.add_argument("--debug", action="store_true", help="Zeige API-JSON-Ausgabe zur Fehlersuche")
|
||||
args = parser.parse_args()
|
||||
|
||||
main(debug=args.debug)
|
||||
BIN
live-terminal-display.png
Normal file
BIN
live-terminal-display.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests
|
||||
rich
|
||||
Reference in New Issue
Block a user