Add IVBEFAClient
This commit is contained in:
parent
710b63045f
commit
c836a3b201
232
efa_client.go
232
efa_client.go
@ -1,19 +1,233 @@
|
||||
package main
|
||||
|
||||
import "cmp"
|
||||
import "encoding/json"
|
||||
import "errors"
|
||||
import "fmt"
|
||||
import "net/http"
|
||||
import "net/url"
|
||||
import "slices"
|
||||
import "strconv"
|
||||
import "strings"
|
||||
import "time"
|
||||
|
||||
type EFAClient interface {
|
||||
GetName() string
|
||||
BuildRequest(string) (*http.Request, error)
|
||||
ParseResponse(*http.Response) (Departures, error)
|
||||
}
|
||||
|
||||
func FetchDepartures(c EFAClient, stopId string) (Departures, error) {
|
||||
req, err := c.BuildRequest(stopId)
|
||||
if err != nil {
|
||||
return Departures{}, err
|
||||
}
|
||||
|
||||
// Send the request, wait max 10 seconds
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return Departures{}, err
|
||||
}
|
||||
|
||||
return c.ParseResponse(res)
|
||||
}
|
||||
|
||||
// JSON unmarshaling types for departure monitor API
|
||||
|
||||
type DMResponse struct {
|
||||
DateTime DMDateTime `json:"dateTime"`
|
||||
DepartureList []DMDeparture `json:"departureList"`
|
||||
}
|
||||
|
||||
type DMDeparture struct {
|
||||
StopName string `json:"stopName"`
|
||||
Platform string `json:"platform"`
|
||||
Countdown string `json:"countdown"`
|
||||
DateTime DMDateTime `json:"dateTime"`
|
||||
RealDateTime *DMDateTime `json:"realDateTime"`
|
||||
ServingLine DMServingLine `json:"servingLine"`
|
||||
OnwardStopSeq []DMStop `json:"onwardStopSeq"`
|
||||
}
|
||||
|
||||
type DMServingLine struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Direction string `json:"direction"`
|
||||
}
|
||||
|
||||
type DMStop struct {
|
||||
PlaceID string `json:"placeID"`
|
||||
Place string `json:"place"`
|
||||
NameWO string `json:"nameWO"`
|
||||
}
|
||||
|
||||
type DMDateTime struct {
|
||||
Hour string `json:"hour"`
|
||||
Minute string `json:"minute"`
|
||||
}
|
||||
|
||||
// Types for JSON marshaling
|
||||
|
||||
type Departures struct {
|
||||
Departures []Departure `json:"departures"`
|
||||
ServerTime string `json:"serverTime"`
|
||||
}
|
||||
|
||||
type Departure struct {
|
||||
Platform string `json:"platform"`
|
||||
Symbol string `json:"symbol"`
|
||||
Direction string `json:"direction"`
|
||||
Leaving string `json:"leaving"`
|
||||
Countdown int `json:"-"`
|
||||
}
|
||||
|
||||
func ParseDMResponse(res *http.Response) (Departures, error) {
|
||||
defer res.Body.Close()
|
||||
var dmResponse DMResponse
|
||||
err := json.NewDecoder(res.Body).Decode(&dmResponse)
|
||||
if err != nil {
|
||||
return Departures{}, err
|
||||
}
|
||||
|
||||
return parseDepartures(dmResponse)
|
||||
}
|
||||
|
||||
func parseDepartures(response DMResponse) (Departures, error) {
|
||||
var ds []Departure
|
||||
|
||||
for _, d := range response.DepartureList {
|
||||
direction := d.ServingLine.Direction
|
||||
// If available, use name of last stop as direction
|
||||
// Note that OnwardStopSeq is only populated if includeCompleteStopSeq=1 is set.
|
||||
if len(d.OnwardStopSeq) != 0 {
|
||||
last := d.OnwardStopSeq[len(d.OnwardStopSeq)-1]
|
||||
if slices.Contains([]string{"5", "6"}, last.PlaceID) {
|
||||
direction = last.NameWO
|
||||
}
|
||||
}
|
||||
|
||||
leaving := fmt.Sprintf("%s min", d.Countdown)
|
||||
countdown, err := strconv.Atoi(d.Countdown)
|
||||
if err != nil {
|
||||
return Departures{}, errors.New("could not parse countdown")
|
||||
}
|
||||
|
||||
isInThePast := countdown < 0
|
||||
if isInThePast {
|
||||
continue
|
||||
}
|
||||
|
||||
if countdown == 0 {
|
||||
leaving = "sofort"
|
||||
}
|
||||
if countdown > 20 {
|
||||
dt := d.DateTime
|
||||
if d.RealDateTime != nil {
|
||||
dt = *d.RealDateTime
|
||||
}
|
||||
leaving = fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute)
|
||||
}
|
||||
|
||||
ds = append(ds, Departure{
|
||||
d.Platform,
|
||||
d.ServingLine.Symbol,
|
||||
direction,
|
||||
leaving,
|
||||
countdown,
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(ds, func(a Departure, b Departure) int {
|
||||
return cmp.Compare(a.Countdown, b.Countdown)
|
||||
})
|
||||
|
||||
dt := response.DateTime
|
||||
|
||||
return Departures{
|
||||
ds,
|
||||
fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var allEfaClients []EFAClient = []EFAClient{
|
||||
IVBEFAClient{},
|
||||
BwegtEFAClient{},
|
||||
VRNEFAClient{},
|
||||
KVVEFAClient{},
|
||||
VAGEFAClient{},
|
||||
VRNEFAClient{},
|
||||
}
|
||||
|
||||
type IVBEFAClient struct {
|
||||
}
|
||||
|
||||
func (c IVBEFAClient) GetName() string {
|
||||
return "IVB"
|
||||
}
|
||||
|
||||
func (c IVBEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
||||
// curl https://smartinfo.ivb.at/api/JSON/PASSAGE'?stopID=64003&count=3'
|
||||
req, err := http.NewRequest("GET", "https://smartinfo.ivb.at/api/JSON/PASSAGE", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := url.Values{}
|
||||
query.Set("stopID", stopId)
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c IVBEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
||||
defer res.Body.Close()
|
||||
var response []IVBResponseElem
|
||||
err := json.NewDecoder(res.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return Departures{}, err
|
||||
}
|
||||
|
||||
var ds []Departure
|
||||
|
||||
for _, responseElem := range response {
|
||||
if responseElem.Smartinfo == nil {
|
||||
continue
|
||||
}
|
||||
si := responseElem.Smartinfo
|
||||
|
||||
leaving := si.Time
|
||||
if leaving == "0 min" {
|
||||
leaving = "sofort"
|
||||
}
|
||||
|
||||
ds = append(ds, Departure{
|
||||
si.Plabel,
|
||||
si.Route,
|
||||
si.Direction,
|
||||
leaving,
|
||||
// TODO
|
||||
0,
|
||||
})
|
||||
}
|
||||
|
||||
return Departures{
|
||||
ds,
|
||||
fmt.Sprintf("%02d:%02d", 0, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// IVB smartinfo API unmarshaling types
|
||||
type IVBResponseElem struct {
|
||||
Smartinfo *IVBSmartInfo `json:"smartinfo"`
|
||||
}
|
||||
|
||||
type IVBSmartInfo struct {
|
||||
Uid string `json:"uid"`
|
||||
Route string `json:"route"`
|
||||
Direction string `json:"direction"`
|
||||
Time string `json:"time"`
|
||||
Plabel string `json:"plabel"`
|
||||
}
|
||||
|
||||
type VRNEFAClient struct {
|
||||
@ -48,6 +262,10 @@ func (c VRNEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c VRNEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
||||
return ParseDMResponse(res)
|
||||
}
|
||||
|
||||
type KVVEFAClient struct {
|
||||
}
|
||||
|
||||
@ -78,6 +296,10 @@ func (c KVVEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c KVVEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
||||
return ParseDMResponse(res)
|
||||
}
|
||||
|
||||
type BwegtEFAClient struct {
|
||||
}
|
||||
|
||||
@ -110,6 +332,10 @@ func (c BwegtEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c BwegtEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
||||
return ParseDMResponse(res)
|
||||
}
|
||||
|
||||
type VAGEFAClient struct {
|
||||
}
|
||||
|
||||
@ -141,3 +367,7 @@ func (c VAGEFAClient) BuildRequest(stopId string) (*http.Request, error) {
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c VAGEFAClient) ParseResponse(res *http.Response) (Departures, error) {
|
||||
return ParseDMResponse(res)
|
||||
}
|
||||
|
||||
150
main.go
150
main.go
@ -1,148 +1,13 @@
|
||||
package main
|
||||
|
||||
import "cmp"
|
||||
import "encoding/json"
|
||||
import "errors"
|
||||
import "fmt"
|
||||
import "log"
|
||||
import "net/http"
|
||||
import "os"
|
||||
import "slices"
|
||||
import "strings"
|
||||
import "strconv"
|
||||
import "sync/atomic"
|
||||
import "time"
|
||||
|
||||
// JSON unmarshaling types for departure monitor API
|
||||
|
||||
type DMResponse struct {
|
||||
DateTime DMDateTime `json:"dateTime"`
|
||||
DepartureList []DMDeparture `json:"departureList"`
|
||||
}
|
||||
|
||||
type DMDeparture struct {
|
||||
StopName string `json:"stopName"`
|
||||
Platform string `json:"platform"`
|
||||
Countdown string `json:"countdown"`
|
||||
DateTime DMDateTime `json:"dateTime"`
|
||||
RealDateTime *DMDateTime `json:"realDateTime"`
|
||||
ServingLine DMServingLine `json:"servingLine"`
|
||||
OnwardStopSeq []DMStop `json:"onwardStopSeq"`
|
||||
}
|
||||
|
||||
type DMServingLine struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Direction string `json:"direction"`
|
||||
}
|
||||
|
||||
type DMStop struct {
|
||||
PlaceID string `json:"placeID"`
|
||||
Place string `json:"place"`
|
||||
NameWO string `json:"nameWO"`
|
||||
}
|
||||
|
||||
type DMDateTime struct {
|
||||
Hour string `json:"hour"`
|
||||
Minute string `json:"minute"`
|
||||
}
|
||||
|
||||
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
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return DMResponse{}, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
var dmResponse DMResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&dmResponse)
|
||||
if err != nil {
|
||||
return DMResponse{}, err
|
||||
}
|
||||
|
||||
return dmResponse, nil
|
||||
}
|
||||
|
||||
// Types for JSON marshaling
|
||||
|
||||
type Departures struct {
|
||||
Departures []Departure `json:"departures"`
|
||||
ServerTime string `json:"serverTime"`
|
||||
}
|
||||
|
||||
type Departure struct {
|
||||
Platform string `json:"platform"`
|
||||
Symbol string `json:"symbol"`
|
||||
Direction string `json:"direction"`
|
||||
Leaving string `json:"leaving"`
|
||||
Countdown int `json:"-"`
|
||||
}
|
||||
|
||||
func ParseDepartures(response DMResponse, allowedPlatform *string) (Departures, error) {
|
||||
var ds []Departure
|
||||
|
||||
for _, d := range response.DepartureList {
|
||||
direction := d.ServingLine.Direction
|
||||
// If available, use name of last stop as direction
|
||||
// Note that OnwardStopSeq is only populated if includeCompleteStopSeq=1 is set.
|
||||
if len(d.OnwardStopSeq) != 0 {
|
||||
last := d.OnwardStopSeq[len(d.OnwardStopSeq)-1]
|
||||
if slices.Contains([]string{"5", "6"}, last.PlaceID) {
|
||||
direction = last.NameWO
|
||||
}
|
||||
}
|
||||
|
||||
leaving := fmt.Sprintf("%s min", d.Countdown)
|
||||
countdown, err := strconv.Atoi(d.Countdown)
|
||||
if err != nil {
|
||||
return Departures{}, errors.New("could not parse countdown")
|
||||
}
|
||||
|
||||
matchesPlatform := allowedPlatform == nil || d.Platform == *allowedPlatform
|
||||
isInThePast := countdown < 0
|
||||
if !matchesPlatform || isInThePast {
|
||||
continue
|
||||
}
|
||||
|
||||
if countdown == 0 {
|
||||
leaving = "sofort"
|
||||
}
|
||||
if countdown > 20 {
|
||||
dt := d.DateTime
|
||||
if d.RealDateTime != nil {
|
||||
dt = *d.RealDateTime
|
||||
}
|
||||
leaving = fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute)
|
||||
}
|
||||
|
||||
ds = append(ds, Departure{
|
||||
d.Platform,
|
||||
d.ServingLine.Symbol,
|
||||
direction,
|
||||
leaving,
|
||||
countdown,
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(ds, func(a Departure, b Departure) int {
|
||||
return cmp.Compare(a.Countdown, b.Countdown)
|
||||
})
|
||||
|
||||
dt := response.DateTime
|
||||
|
||||
return Departures{
|
||||
ds,
|
||||
fmt.Sprintf("%02s:%02s", dt.Hour, dt.Minute),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
password := os.Getenv("VRNP_PASSWORD")
|
||||
@ -177,23 +42,20 @@ func main() {
|
||||
}
|
||||
stopId := query["stop_id"][0]
|
||||
|
||||
/*
|
||||
var platform *string = nil
|
||||
if query["platform"] != nil && len(query["platform"]) != 0 {
|
||||
platform = &query["platform"][0]
|
||||
}
|
||||
*/
|
||||
|
||||
c := efaClients[(efaClient.Add(1) - 1) % uint64(len(efaClients))]
|
||||
ds, err := FetchDepartures(c, stopId)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
departures, err := ParseDepartures(ds, platform)
|
||||
c := efaClients[(efaClient.Add(1)-1)%uint64(len(efaClients))]
|
||||
departures, err := FetchDepartures(c, stopId)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// TODO: Filter for platform here
|
||||
|
||||
// Does not handle multiple media types
|
||||
if r.Header.Get("Accept") == "application/json" {
|
||||
@ -203,7 +65,7 @@ func main() {
|
||||
// plain text
|
||||
for _, d := range departures.Departures {
|
||||
fmt.Fprintf(w, "%2s %-11s %6s\n",
|
||||
d.Symbol,
|
||||
d.Symbol[:min(2, len(d.Symbol))],
|
||||
d.Direction[:min(11, len(d.Direction))],
|
||||
d.Leaving,
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user