Merge branch 'main' of https://git.miaig.dev/mia/hafas-terminal-app
This commit is contained in:
commit
e66d33eefa
3 changed files with 144 additions and 58 deletions
110
README.md
110
README.md
|
|
@ -1,23 +1,95 @@
|
||||||
# hafas-terminal-app
|
# hafas-terminal-app
|
||||||
|
|
||||||
## Description
|
A Python-based terminal application for accessing public transport information via the HAFAS API. This tool provides station-to-station route planning and real-time arrival/departure information for public transport networks.
|
||||||
The goal is to access the Hafas API to retrieve Data about the public transport network. Most important is to achieve Station to Station Routeplanning and Station Arrival/Departure Information.
|
|
||||||
|
|
||||||
## Dependencies
|
## Features
|
||||||
- Python
|
|
||||||
- prompt_toolkit (tui)
|
|
||||||
- kivy (mobile-ui) ((optional!!!))
|
|
||||||
- requests
|
|
||||||
- json
|
|
||||||
- Hafas API Endpoint
|
|
||||||
- Git
|
|
||||||
|
|
||||||
## Timetable
|
- **Route Planning**: Find optimal routes between stations
|
||||||
- ~~22.04.2026 - Short project presentation: 3min per Group, incl. UseCase, GitProject, Issues~~
|
- **Station Information**: View real-time arrivals and departures
|
||||||
- ~~29.04.2026 - Scrum-Round mia/hafas-terminal-app#1~~
|
- **Terminal UI**: User-friendly command-line interface using prompt_toolkit
|
||||||
- ~~06.05.2026 - Scrum-Round mia/hafas-terminal-app#2, optionally mia/hafas-terminal-app#3 and mia/hafas-terminal-app#4~~
|
- **Multiple Transport Methods**: Support for various public transport modes
|
||||||
- 13.05.2026 - Scrum-Round #3, #4, #5
|
|
||||||
- 20.05.2026 - Scrum-Round
|
## Requirements
|
||||||
- 27.05.2026 - Scrum-Round
|
|
||||||
- 03.06.2026 - Scrum-Round
|
### Core Dependencies
|
||||||
- 10.06.2026 - Presentations
|
- **Python 3.7+**: Programming language
|
||||||
|
- **Hafas API Endpoint**: Access to a HAFAS public transport API
|
||||||
|
|
||||||
|
### Python Packages
|
||||||
|
- `prompt_toolkit`: Terminal user interface (TUI)
|
||||||
|
- `requests`: HTTP library for API communication
|
||||||
|
- `kivy` (optional): Mobile UI support for future mobile versions
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
- `Git`: Version control
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option 1: Using Nix (Recommended)
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://git.miaig.dev/mia/hafas-terminal-app.git
|
||||||
|
cd hafas-terminal-app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Enter the Nix development environment:
|
||||||
|
```bash
|
||||||
|
nix develop # Modern Nix with flakes
|
||||||
|
# or
|
||||||
|
nix-shell # Traditional Nix
|
||||||
|
```
|
||||||
|
|
||||||
|
All dependencies will be automatically configured.
|
||||||
|
|
||||||
|
### Option 2: Using pip
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://git.miaig.dev/mia/hafas-terminal-app.git
|
||||||
|
cd hafas-terminal-app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a virtual environment:
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Run the application:
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Navigate between stations and search for routes
|
||||||
|
- View departure and arrival times
|
||||||
|
- Display detailed journey information
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
hafas-terminal-app/
|
||||||
|
├── main.py # Application entry point
|
||||||
|
├── backend.py # Backend API communication logic
|
||||||
|
├── route_planning.py # Route planning algorithms
|
||||||
|
├── station_monitor.py # Station monitoring functionality
|
||||||
|
├── flake.nix # Nix environment configuration
|
||||||
|
├── README.md # This file
|
||||||
|
└── docs/ # Documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is provided under the terms specified in the [LICENSE](./LICENSE) file.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit issues and pull requests to help improve the application.
|
||||||
42
backend.py
42
backend.py
|
|
@ -9,7 +9,8 @@ The application will provide routing information and station departures/arrivals
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
import re
|
||||||
|
from datetime import date, datetime
|
||||||
from prompt_toolkit import Application
|
from prompt_toolkit import Application
|
||||||
from prompt_toolkit.completion import WordCompleter
|
from prompt_toolkit.completion import WordCompleter
|
||||||
|
|
||||||
|
|
@ -43,6 +44,13 @@ class HafasClient:
|
||||||
headers=self.headers)
|
headers=self.headers)
|
||||||
return json.loads(res.text)
|
return json.loads(res.text)
|
||||||
|
|
||||||
|
def journeyRequest(self, origin, destination, via=None):
|
||||||
|
time = datetime.now()
|
||||||
|
res = self.session.post(self.baseUrl,
|
||||||
|
data=json.dumps({"svcReqL": [{"req": {"arrLocL": [{"lid": f"A=1@L={destination}@"}], "viaLocL": via or [], "depLocL": [{"lid": f"A=1@L={origin}@"}], "outDate": time.strftime("%Y%m%d"), "outTime": time.strftime("%H%M%S"), "jnyFltrL": [{"type": "PROD", "mode": "INC", "value": "4087"}], "minChgTime": 0, "maxChg": -1, "numF": 1}, "meth": "TripSearch"}], "client": self.clientInfo, "ver": self.version, "lang": self.language, "auth": self.auth}),
|
||||||
|
headers=self.headers)
|
||||||
|
return json.loads(res.text)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def getStationNames(self, stationString):
|
def getStationNames(self, stationString):
|
||||||
|
|
@ -75,20 +83,13 @@ class HafasClient:
|
||||||
# - Departure/arrival time: departure["stbStop"]["dTimeS"] or ["aTimeS"]
|
# - Departure/arrival time: departure["stbStop"]["dTimeS"] or ["aTimeS"]
|
||||||
departures.append({
|
departures.append({
|
||||||
"name": departure.get("dirTxt"),
|
"name": departure.get("dirTxt"),
|
||||||
"line": (
|
"line": re.search(r'#ZB#([^#]+)', departure.get("jid")).group(1).strip(),
|
||||||
product.get("nameS")
|
|
||||||
or product.get("number")
|
|
||||||
or product.get("name")
|
|
||||||
or product.get("prodCtx", {}).get("line")
|
|
||||||
or product.get("prodCtx", {}).get("name")
|
|
||||||
or product.get("prodCtx", {}).get("nameS")
|
|
||||||
),
|
|
||||||
"departure_time": self._format_time(stop.get(time_key)),
|
"departure_time": self._format_time(stop.get(time_key)),
|
||||||
"departure_time_raw": stop.get(time_key),
|
"departure_time_raw": stop.get(time_key),
|
||||||
"departure_time_real": self._format_time(stop.get(time_key_real)),
|
"departure_time_real": self._format_time(stop.get(time_key_real)),
|
||||||
"departure_time_real_raw": stop.get(time_key_real),
|
"departure_time_real_raw": stop.get(time_key_real),
|
||||||
"jid": departure.get("jid"),
|
"jid": departure.get("jid"),
|
||||||
"raw": departure,
|
# "raw": departure,
|
||||||
})
|
})
|
||||||
|
|
||||||
return departures
|
return departures
|
||||||
|
|
@ -114,13 +115,25 @@ class HafasClient:
|
||||||
trip = self.getTrip(departures[0]["jid"])
|
trip = self.getTrip(departures[0]["jid"])
|
||||||
__import__('pprint').pprint(trip)
|
__import__('pprint').pprint(trip)
|
||||||
|
|
||||||
def moniterTest(self):
|
def monitorTest(self):
|
||||||
stationInput = input("Enter Station Name: ")
|
stationInput = input("Enter Station Name: ")
|
||||||
station = self.getStationNames(stationInput)
|
station = self.getStationNames(stationInput)
|
||||||
print(station)
|
print(station)
|
||||||
# departures = self.getArrDep(station[0][1], arrdep="DEP", count=1)
|
# departures = self.getArrDep(station[0][1], arrdep="DEP", count=1)
|
||||||
departures = self.arrDepRequest(station[0][1], arrdep="DEP", count=1)
|
print("\n)------------------\n")
|
||||||
print(departures)
|
departures = self.getArrDep(station[0][1], arrdep="DEP", count=2)
|
||||||
|
__import__('pprint').pprint(departures)
|
||||||
|
print("\n)------------------\n")
|
||||||
|
arrivals = self.getArrDep(station[0][1], arrdep="ARR", count=2)
|
||||||
|
__import__('pprint').pprint(arrivals)
|
||||||
|
#arrivalsRaw = self.arrDepRequest(station[0][1], arrdep="ARR", count=1)
|
||||||
|
#__import__('pprint').pprint(arrivalsRaw)
|
||||||
|
|
||||||
|
def journeyTest(self):
|
||||||
|
origin = self.getStationNames(input("Origin: "))[0][1]
|
||||||
|
destination = self.getStationNames(input("Destination: "))[0][1]
|
||||||
|
journeys = self.journeyRequest(origin, destination)
|
||||||
|
__import__('pprint').pprint(journeys)
|
||||||
|
|
||||||
|
|
||||||
# streq = self.stationRequest(station)["svcResL"][0]["res"]["match"]["locL"]
|
# streq = self.stationRequest(station)["svcResL"][0]["res"]["match"]["locL"]
|
||||||
|
|
@ -140,4 +153,5 @@ class HafasClient:
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
client = HafasClient()
|
client = HafasClient()
|
||||||
# client.runTests()
|
# client.runTests()
|
||||||
client.moniterTest()
|
# client.monitorTest()
|
||||||
|
client.journeyTest()
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ from prompt_toolkit.layout.containers import Window, VSplit, HSplit
|
||||||
from prompt_toolkit.layout.controls import BufferControl
|
from prompt_toolkit.layout.controls import BufferControl
|
||||||
from prompt_toolkit.key_binding import KeyBindings
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
import threading
|
import threading
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def run_station_monitor(hafas: backend.HafasClient | None = None):
|
def run_station_monitor(hafas: backend.HafasClient | None = None):
|
||||||
|
|
@ -69,37 +68,38 @@ def run_station_monitor(hafas: backend.HafasClient | None = None):
|
||||||
def handle_enter(event):
|
def handle_enter(event):
|
||||||
global app_state
|
global app_state
|
||||||
|
|
||||||
user_input = userinputBuffer.text.strip()
|
user_input = userinputBuffer.text.strip()
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
app_state = "MONITOR"
|
||||||
|
return
|
||||||
|
|
||||||
if user_input:
|
|
||||||
app_state = "RESULTS"
|
app_state = "RESULTS"
|
||||||
results = hafas.getStationNames(user_input)
|
results = hafas.getStationNames(user_input)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
arrivalBuffer.text = f"Keine Stationen gefunden für: {user_input}"
|
arrivalBuffer.text = f"Keine Stationen gefunden für: {user_input}"
|
||||||
departureBuffer.text= ""
|
departureBuffer.text = ""
|
||||||
userinputBuffer.text=""
|
|
||||||
app_state = "MONITOR"
|
|
||||||
|
|
||||||
else:
|
|
||||||
station_name = results[0][1]
|
|
||||||
station_id = results[0][0]
|
|
||||||
|
|
||||||
arrivals = hafas.getArrDep(station_id, arrdep="ARR", count=3)
|
|
||||||
arrivalBuffer.text = f"Ankünfte für {station_name}:\n"
|
|
||||||
for arrival in arrivals:
|
|
||||||
arrivalBuffer.insert_text(f"\n {arrival}")
|
|
||||||
|
|
||||||
departures = hafas.getArrDep(station_id, arrdep="DEP", count=3)
|
|
||||||
departureBuffer.text = f"Abfahrten für {station_name}:\n"
|
|
||||||
for departure in departures:
|
|
||||||
departureBuffer.insert_text(f"\n {departure}")
|
|
||||||
|
|
||||||
|
|
||||||
userinputBuffer.text = ""
|
userinputBuffer.text = ""
|
||||||
|
app_state = "MONITOR"
|
||||||
|
return
|
||||||
|
|
||||||
else:
|
station_name = results[0][0]
|
||||||
app_state = "MONITOR"
|
station_id = results[0][1]
|
||||||
|
|
||||||
|
arrivals = hafas.getArrDep(station_id, arrdep="ARR", count=5)
|
||||||
|
arrivalBuffer.text = (
|
||||||
|
f"Ankünfte für {station_name}:\n"
|
||||||
|
+ "\n".join(f" {arrival["departure_time_real"]} {arrival["line"]} <-- {arrival["name"]}" for arrival in arrivals)
|
||||||
|
)
|
||||||
|
|
||||||
|
departures = hafas.getArrDep(station_id, arrdep="DEP", count=5)
|
||||||
|
departureBuffer.text = (
|
||||||
|
f"Abfahrten für {station_name}:\n"
|
||||||
|
+ "\n".join(f" {departure["departure_time_real"]} {departure["line"]} --> {departure["name"]}" for departure in departures)
|
||||||
|
)
|
||||||
|
|
||||||
|
userinputBuffer.text = ""
|
||||||
|
|
||||||
|
|
||||||
@keyB.add("c-q")
|
@keyB.add("c-q")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue