From 214a950ac22b70da43a9e4cc823be0bc53d6ebfe Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Sun, 22 Feb 2026 11:35:27 +0100 Subject: [PATCH] feat(skill): add rvv-regensburg skill with real-time EFA API support --- rvv-regensburg.skill | Bin 0 -> 2662 bytes skills/rvv-regensburg/SKILL.md | 45 +++++++ skills/rvv-regensburg/scripts/rvv_query.py | 130 +++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 rvv-regensburg.skill create mode 100644 skills/rvv-regensburg/SKILL.md create mode 100755 skills/rvv-regensburg/scripts/rvv_query.py diff --git a/rvv-regensburg.skill b/rvv-regensburg.skill new file mode 100644 index 0000000000000000000000000000000000000000..f2611cc778c876bae8b8a9de6900893db63fab8f GIT binary patch literal 2662 zcmZ`*XHXLg5>6llL`tZks)XJG0)liTf|P*Cg8(93=|#Fg07Zz>g`k8Y%@>N&n=}m) zkluM`5TuC|fdJC09Pe)C=H~9b-5)!3 z7(Mv|+``|rY7%hwx6yHza#1dP&F{HK03hSe_@xPw-pt#G7#71bnC!PSnL%ID;bQh} zKY?3#?K!}X3x}u8YbEzW0y~)=P53oN4TFrHv!TBXOjJC8%sc@7##zx|_!&YFJ z1GirJcyVp2#e&AHT=_{ww19Dici~9jM=K0jx7()MnZB`qjvnJ4ckKlo0Y6RF_wB}Y z_LF_Xm^J!_GeDP^$iX=FHO2yklBn$ZpY>zCffq7U#0wc_CB@?Lu}#3z@X*;5Ny#^{ zo(|G$Mlsonj;kW^akxBd5@b_n+z5ulWKM8PfykBHo8-9W+s7R}EslW)y zdYB@cSt9T)(5Ii$DIK^9*6@SrmwRmv#8Fc4#~>H!8jdT$Co=fP6+^!KWax zsD4|cK*1+3WmX`4Z9Pnk)%tGXU8SQ%;i8rvBg-*lGM^Q<)WW9%AC5HVQDFLf{{HSl zU&4*nnbln@Psr8I-+V04qoGBN0WO}v(QkWAQ8b60 z%+b$U5@A6~?coB$f@C>iRW_&jx}?ZTpx>)EM%E#I9l?gHx<|z|mF;TRt#WgJH)19d z`2zW?Fj$er1Ou55#{!7?SCh0RA`LUY4$kj)x{@^PkLGb>*=8p2F3PId2UjRK>PTHI zvX;3iX3#2F`8ct;K$b2Y)9lcKgRg)VMU==1oANDwJzmbz$v*vuq#)^>e4ptc`G4!*+hVp67GO`N22sPt&v3&gUA zW@r-QS5F{HdA7Ir9ig22)RD_f{yF6g!#;j7{qa)8i{{RKy{7Lu?A-0*Pj*kU)fbbJ zyixqUq?LU<)bOnIJ6_cc=C71V&*oO9+IZ9G44|D}5=Um*FFotalZO@x#Zvd$65|+$ zUh|`yRKEN!7`ktm1A@Mnl2PIvXcKK2E7cPAB%wX1#){sTr#zqc&oQt+8MgekN@DE$ zQKye9=7aLc%k9%cwCM6SsM!5`vWdZbObj$p)o!<<^xQ=j$P8Pp8|wyWJaycVawwGA-ST{Yd;Z5V zVP3a%zUAhni%m|46JK4oBBTwsk-SA6+PGnT!Un5a+Jd5G!YgUn-iIGPmw&!oJci!! z-pXXQ@_m4;IPU6l8bs7vX1{7SX^37rilF5SqRGi#XWW=w-F%9u&2RPjsXivKMB@hKF=p|JYZ zo!+KmRlU!v(*)mB{mQjiF|7>5xVr+AE>JS~*t{L{E_b<-U2TZS?cl}x-d6~Ty`+&| z9eQ-!x8xQU$FsHHi6g##E4tKP@YM>m zS@>k4JB2Xbi#PEJf_HmaRcEd}U1N1UWwg=GIOMe-98B0TElE(7S{BSasa`qYJ#5WN zleT_2jofF^bU+>4lPns~M4U>4#Si#z=~3S1|@U!i~bpIDy*Q zo1MB}Xj8!Nd$?qBobo0s zUDL-dtOXw;jOke1#=WCK9)dAT@2z0Q#~COy2Xl|ygGPtasy5n`DmEu9kc*WpDG3+W-@mEs1uI!%wJV*pZ`p{?m}o zPwXfUlbdIdI}KBj3gW55LG1iY0>XQm|Il%n6#=|qmLVd{5}I0DP#sbkk-Cf8V#0U>#USvRd!;DyQTQJn-&>MQFRJDwTBfb_gr0sXPCQ zzwb+M-veXxrRCAB1!Wc~cBv@39K9R_ z{9)7U4020FH8btUc_l_2ZCbl-+XrAWgBuW>ESrd^7m(qIqg#lm9>3*3BzPtZjd0Q3kzwRjd0^alK{`9F|}3kbY "Regensburg, Hauptbahnhof"). diff --git a/skills/rvv-regensburg/scripts/rvv_query.py b/skills/rvv-regensburg/scripts/rvv_query.py new file mode 100755 index 0000000..0a1e5bd --- /dev/null +++ b/skills/rvv-regensburg/scripts/rvv_query.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +import requests +import json +import argparse +import sys + +BASE_URL = "https://efa.rvv.de/efa/" +VERSION = "10.5.17.3" + +def query_stop(name): + params = { + "name_sf": name, + "outputFormat": "rapidJSON", + "type_sf": "any", + "locationInfoActive": "1", + "version": VERSION + } + response = requests.get(f"{BASE_URL}XML_STOPFINDER_REQUEST", params=params) + return response.json() + +def query_departures(stop_id, count=10): + params = { + "name_dm": stop_id, + "type_dm": "stop", + "outputFormat": "rapidJSON", + "itdDateTimeDepArr": "dep", + "useRealtime": "1", + "mode": "direct", + "depSequence": count, + "version": VERSION + } + response = requests.get(f"{BASE_URL}XML_DM_REQUEST", params=params) + return response.json() + +def query_trip(origin_id, destination_id, count=4): + params = { + "name_origin": origin_id, + "type_origin": "stop", + "name_destination": destination_id, + "type_destination": "stop", + "outputFormat": "rapidJSON", + "itdTripDateTimeDepArr": "dep", + "calcNumberOfTrips": count, + "useRealtime": "1", + "mode": "direct", + "version": VERSION + } + response = requests.get(f"{BASE_URL}XML_TRIP_REQUEST2", params=params) + return response.json() + +def main(): + parser = argparse.ArgumentParser(description="Query RVV Regensburg API") + subparsers = parser.add_subparsers(dest="command", help="Command to run") + + # Stop Finder + stop_parser = subparsers.add_parser("stop", help="Find a stop ID") + stop_parser.add_argument("name", help="Name of the stop") + + # Departure Monitor + dep_parser = subparsers.add_parser("departures", help="Get departures for a stop") + dep_parser.add_argument("stop_id", help="Stop ID (e.g., de:09362:11010)") + dep_parser.add_argument("--count", type=int, default=10, help="Number of departures") + + # Trip Planning + trip_parser = subparsers.add_parser("trip", help="Plan a trip between two stops") + trip_parser.add_argument("origin_id", help="Origin Stop ID") + trip_parser.add_argument("destination_id", help="Destination Stop ID") + trip_parser.add_argument("--count", type=int, default=4, help="Number of trips") + + args = parser.parse_args() + + if args.command == "stop": + result = query_stop(args.name) + points = result.get("locations", []) + output = [] + for p in points: + if p.get("type") == "stop": + output.append({ + "id": p["id"], + "stopId": p.get("properties", {}).get("stopId") or p.get("id"), + "name": p["name"] + }) + print(json.dumps(output, indent=2, ensure_ascii=False)) + elif args.command == "departures": + result = query_departures(args.stop_id, args.count) + # EFA rapidJSON uses 'stopEvents' for departures + stop_events = result.get("stopEvents", []) + output = [] + for event in stop_events: + transport = event.get("transportation", {}) + departure_time = event.get("departureTimePlanned") + departure_time_rt = event.get("departureTimeEstimated") + output.append({ + "line": transport.get("number"), + "direction": transport.get("destination", {}).get("name"), + "base_time": departure_time[11:16] if departure_time else None, + "rt_time": departure_time_rt[11:16] if departure_time_rt else None, + "delay": event.get("delay", 0) + }) + print(json.dumps(output, indent=2, ensure_ascii=False)) + elif args.command == "trip": + result = query_trip(args.origin_id, args.destination_id, args.count) + # EFA rapidJSON uses 'journeys' for trips + journeys = result.get("journeys", []) + output = [] + for j in journeys: + legs = j.get("legs", []) + trip_info = { + "legs": [] + } + for leg in legs: + origin = leg.get("origin", {}) + dest = leg.get("destination", {}) + transport = leg.get("transportation", {}) + trip_info["legs"].append({ + "from": origin.get("name"), + "to": dest.get("name"), + "line": transport.get("number"), + "dep_planned": origin.get("departureTimePlanned", "")[11:16], + "dep_est": origin.get("departureTimeEstimated", "")[11:16], + "arr_planned": dest.get("arrivalTimePlanned", "")[11:16], + "arr_est": dest.get("arrivalTimeEstimated", "")[11:16] + }) + output.append(trip_info) + print(json.dumps(output, indent=2, ensure_ascii=False)) + else: + parser.print_help() + +if __name__ == "__main__": + main()