7 Commits
Author SHA1 Message Date
paul 5d5b893948 Sort departures by countdown
Build image / build-image (push) Successful in 2m45s
2025-04-25 17:09:29 +02:00
paul 4a73099460 Draw tiny train when train is arriving 2025-04-25 16:33:00 +02:00
paul addc90d856 Bump version
Build image / build-image (push) Successful in 2m58s
2025-04-25 13:39:17 +02:00
paul d23be3062a Merge pull request 'Send EFA requests to multiple servers' (#2) from feature/multiple-servers into main
Reviewed-on: #2
2025-04-25 13:38:30 +02:00
paul 740b654be6 Add bwegt efa client 2025-04-25 13:37:01 +02:00
paul dcc3cfa44f Send requests to different servers using round-robin 2025-04-25 12:30:19 +02:00
paul ae7b2f170a Add EFAClient interface 2025-04-25 12:04:47 +02:00
4 changed files with 117 additions and 9 deletions
+1
View File
@@ -1,3 +1,4 @@
result result
vrnp vrnp
main
*.swp *.swp
+10 -5
View File
@@ -176,14 +176,19 @@ void logic_loop() {
int line = 0; int line = 0;
for (JsonVariant departure : departures["departures"].as<JsonArray>()) { for (JsonVariant departure : departures["departures"].as<JsonArray>()) {
const char *directionStr = departure["direction"].as<const char *>();
display.setCursor(MAIN_X, MAIN_Y + (CY + 3) * line); display.setCursor(MAIN_X, MAIN_Y + (CY + 3) * line);
display.printf("%2s %-15.15s %6s", display.printf("%2s %-15.15s ",
departure["symbol"].as<const char*>(), departure["symbol"].as<const char*>(),
directionStr, departure["direction"].as<const char *>()
departure["leaving"].as<const char*>()
); );
if (departure["leaving"].as<String>().equals("sofort")) {
int16_t x = display.getCursorX();
int16_t y = display.getCursorY();
display.drawBitmap(x + 6 * CX - tiny_train_dims[0], y, tiny_train[0], tiny_train_dims[0], tiny_train_dims[1], COLOR_TEXT);
} else {
display.printf("%6s", departure["leaving"].as<const char*>());
}
line++; line++;
} }
+1 -1
View File
@@ -16,7 +16,7 @@
vrnp-static = pkgs.buildGoModule { vrnp-static = pkgs.buildGoModule {
pname = "vrnp"; pname = "vrnp";
version = "0.0.5"; version = "0.0.7";
vendorHash = null; vendorHash = null;
# For building the package, we use only the files not ignored by Git as inputs. # For building the package, we use only the files not ignored by Git as inputs.
+105 -3
View File
@@ -1,5 +1,6 @@
package main package main
import "cmp"
import "encoding/json" import "encoding/json"
import "errors" import "errors"
import "fmt" import "fmt"
@@ -8,7 +9,9 @@ import "net/http"
import "net/url" import "net/url"
import "os" import "os"
import "slices" import "slices"
import "strings"
import "strconv" import "strconv"
import "sync/atomic"
import "time" import "time"
// JSON unmarshaling types for departure monitor API // JSON unmarshaling types for departure monitor API
@@ -43,11 +46,23 @@ type DMDateTime struct {
Minute string `json:"minute"` Minute string `json:"minute"`
} }
func FetchDepartures(stopId string) (DMResponse, error) { type EFAClient interface {
GetName() string
BuildRequest(string) (*http.Request, error)
}
type VRNEFAClient struct {
}
func (c VRNEFAClient) GetName() string {
return "VRN"
}
func (c VRNEFAClient) BuildRequest(stopId string) (*http.Request, error) {
// Create request object // Create request object
req, err := http.NewRequest("GET", "https://www.vrn.de/mngvrn/XML_DM_REQUEST", nil) req, err := http.NewRequest("GET", "https://www.vrn.de/mngvrn/XML_DM_REQUEST", nil)
if err != nil { if err != nil {
return DMResponse{}, err return nil, err
} }
// Configure our request // Configure our request
@@ -65,6 +80,77 @@ func FetchDepartures(stopId string) (DMResponse, error) {
query.Set("useRealtime", "1") query.Set("useRealtime", "1")
req.URL.RawQuery = query.Encode() req.URL.RawQuery = query.Encode()
return req, nil
}
type KVVEFAClient struct {
}
func (c KVVEFAClient) GetName() string {
return "KVV"
}
func (c KVVEFAClient) BuildRequest(stopId string) (*http.Request, error) {
form := url.Values{}
form.Set("action", "XSLT_DM_REQUEST")
form.Set("name_dm", stopId)
form.Set("type_dm", "stop")
form.Set("useRealtime", "1")
form.Set("limit", "10")
form.Set("mode", "direct")
form.Set("useRealtime", "1")
form.Set("outputFormat", "json")
body := strings.NewReader(form.Encode())
req, err := http.NewRequest("POST", "https://www.kvv.de/tunnelEfaDirect.php", body)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "coolio/1.0")
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req, nil
}
type BwegtEFAClient struct {
}
func (c BwegtEFAClient) GetName() string {
return "bwegt"
}
func (c BwegtEFAClient) BuildRequest(stopId string) (*http.Request, error) {
// Create request object
req, err := http.NewRequest("GET", "https://www.bwegt.de/bwegt-efa/XML_DM_REQUEST", nil)
if err != nil {
return nil, err
}
// Configure our request
query := url.Values{}
query.Set("coordOutputFormat", "EPSG:4326")
query.Set("depType", "stopEvents")
query.Set("includeCompleteStopSeq", "0")
query.Set("limit", "10")
query.Set("locationServerActive", "0")
query.Set("mode", "direct")
query.Set("name_dm", stopId)
query.Set("outputFormat", "json")
query.Set("type_dm", "stop")
query.Set("useOnlyStops", "1")
query.Set("useRealtime", "1")
req.URL.RawQuery = query.Encode()
return req, nil
}
func FetchDepartures(c EFAClient, stopId string) (DMResponse, error) {
req, err := c.BuildRequest(stopId)
if err != nil {
return DMResponse{}, err
}
// Send the request, wait max 10 seconds // Send the request, wait max 10 seconds
client := http.Client{ client := http.Client{
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
@@ -95,6 +181,7 @@ type Departure struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
Direction string `json:"direction"` Direction string `json:"direction"`
Leaving string `json:"leaving"` Leaving string `json:"leaving"`
Countdown int
} }
func ParseDepartures(response DMResponse, allowedPlatform *string) (Departures, error) { func ParseDepartures(response DMResponse, allowedPlatform *string) (Departures, error) {
@@ -139,9 +226,14 @@ func ParseDepartures(response DMResponse, allowedPlatform *string) (Departures,
d.ServingLine.Symbol, d.ServingLine.Symbol,
direction, direction,
leaving, leaving,
countdown,
}) })
} }
slices.SortFunc(ds, func(a Departure, b Departure) int {
return cmp.Compare(a.Countdown, b.Countdown)
})
return Departures{ds}, nil return Departures{ds}, nil
} }
@@ -151,6 +243,14 @@ func main() {
panic("Required environment variable VRNP_PASSWORD is not set") panic("Required environment variable VRNP_PASSWORD is not set")
} }
// Use round-robin to send incoming requests to different servers
var efaClient atomic.Uint64
efaClients := []EFAClient{
BwegtEFAClient{},
VRNEFAClient{},
KVVEFAClient{},
}
http.HandleFunc("/departures", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/departures", func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth() user, pass, ok := r.BasicAuth()
if !(ok && user == "admin" && pass == password) { if !(ok && user == "admin" && pass == password) {
@@ -172,7 +272,9 @@ func main() {
platform = &query["platform"][0] platform = &query["platform"][0]
} }
ds, err := FetchDepartures(stopId) c := efaClients[(efaClient.Add(1) - 1) % uint64(len(efaClients))]
log.Print(c.GetName())
ds, err := FetchDepartures(c, stopId)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return