From ef4845a044d4f366e39433a1fcfbd55d82bb5171 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 10 Oct 2021 17:12:52 +0200 Subject: [PATCH] Http: split handlers into separate file --- server/http.go | 330 ++--------------------------------------- server/http_handler.go | 288 +++++++++++++++++++++++++++++++++++ server/uds.go | 2 +- 3 files changed, 300 insertions(+), 320 deletions(-) create mode 100644 server/http_handler.go diff --git a/server/http.go b/server/http.go index acc437c7c..e9a68bc5f 100644 --- a/server/http.go +++ b/server/http.go @@ -1,20 +1,13 @@ package server import ( - "encoding/json" "fmt" - "html/template" "io/fs" "net/http" - "os" - "strconv" "time" - "github.com/evcc-io/evcc/api" - "github.com/evcc-io/evcc/core/loadpoint" "github.com/evcc-io/evcc/core/site" "github.com/evcc-io/evcc/util" - "github.com/evcc-io/evcc/util/test" "github.com/gorilla/handlers" "github.com/gorilla/mux" ) @@ -22,12 +15,6 @@ import ( // Assets is the embedded assets file system var Assets fs.FS -const result = "result" - -type errorJSON struct { - Error string `json:"error"` -} - type route struct { Methods []string Pattern string @@ -48,300 +35,6 @@ func routeLogger(inner http.Handler) http.HandlerFunc { } } -func indexHandler(site site.API) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html; charset=UTF-8") - - indexTemplate, err := fs.ReadFile(Assets, "index.html") - if err != nil { - log.FATAL.Print("httpd: failed to load embedded template:", err.Error()) - log.FATAL.Fatal("Make sure templates are included using the `release` build tag or use `make build`") - } - - t, err := template.New("evcc").Delims("[[", "]]").Parse(string(indexTemplate)) - if err != nil { - log.FATAL.Fatal("httpd: failed to create main page template:", err.Error()) - } - - if err := t.Execute(w, map[string]interface{}{ - "Version": Version, - "Commit": Commit, - "Configured": len(site.LoadPoints()), - }); err != nil { - log.ERROR.Println("httpd: failed to render main page:", err.Error()) - } - }) -} - -// jsonHandler is a middleware that decorates responses with JSON and CORS headers -func jsonHandler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - h.ServeHTTP(w, r) - }) -} - -func jsonResponse(w http.ResponseWriter, r *http.Request, content interface{}) { - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(content); err != nil { - log.ERROR.Printf("httpd: failed to encode JSON: %v", err) - } -} - -// HealthHandler returns current charge mode -func HealthHandler(site site.API) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if !site.Healthy() { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - fmt.Fprintln(w, "OK") - } -} - -// TemplatesHandler returns current charge mode -func TemplatesHandler() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - class, ok := vars["class"] - if !ok { - w.WriteHeader(http.StatusBadRequest) - return - } - - type template = struct { - Name string `json:"name"` - Sample string `json:"template"` - } - - res := make([]template, 0) - for _, conf := range test.ConfigTemplates(class) { - typedSample := fmt.Sprintf("type: %s\n%s", conf.Type, conf.Sample) - t := template{ - Name: conf.Name, - Sample: typedSample, - } - res = append(res, t) - } - - jsonResponse(w, r, res) - } -} - -// StateHandler returns current charge mode -func StateHandler(cache *util.Cache) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - res := cache.State() - for _, k := range []string{"availableVersion", "releaseNotes"} { - delete(res, k) - } - jsonResponse(w, r, res) - } -} - -// ChargeModeHandler updates charge mode -func ChargeModeHandler(lp loadpoint.API) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - mode, err := api.ChargeModeString(vars["value"]) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - jsonResponse(w, r, errorJSON{Error: err.Error()}) - return - } - - lp.SetMode(mode) - - jsonResponse(w, r, map[string]interface{}{result: lp.GetMode()}) - } -} - -// TargetSoCHandler updates target soc -func TargetSoCHandler(lp loadpoint.API) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - soc, err := strconv.ParseInt(vars["value"], 10, 32) - if err == nil { - err = lp.SetTargetSoC(int(soc)) - } - - if err != nil { - w.WriteHeader(http.StatusBadRequest) - jsonResponse(w, r, errorJSON{Error: err.Error()}) - return - } - - jsonResponse(w, r, map[string]interface{}{result: lp.GetTargetSoC()}) - } -} - -// MinSoCHandler updates minimum soc -func MinSoCHandler(lp loadpoint.API) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - soc, err := strconv.ParseInt(vars["value"], 10, 32) - if err == nil { - err = lp.SetMinSoC(int(soc)) - } - - if err != nil { - w.WriteHeader(http.StatusBadRequest) - jsonResponse(w, r, errorJSON{Error: err.Error()}) - return - } - - jsonResponse(w, r, map[string]interface{}{result: lp.GetMinSoC()}) - } -} - -// MinCurrentHandler updates minimum current -func MinCurrentHandler(lp loadpoint.API) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - current, err := strconv.ParseFloat(vars["value"], 64) - if err == nil { - lp.SetMinCurrent(current) - } else { - w.WriteHeader(http.StatusBadRequest) - jsonResponse(w, r, errorJSON{Error: err.Error()}) - return - } - - jsonResponse(w, r, map[string]interface{}{result: lp.GetMinCurrent()}) - } -} - -// MaxCurrentHandler updates maximum current -func MaxCurrentHandler(lp loadpoint.API) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - current, err := strconv.ParseFloat(vars["value"], 64) - if err == nil { - lp.SetMaxCurrent(current) - } else { - w.WriteHeader(http.StatusBadRequest) - jsonResponse(w, r, errorJSON{Error: err.Error()}) - return - } - - jsonResponse(w, r, map[string]interface{}{result: lp.GetMaxCurrent()}) - } -} - -// PhasesHandler updates minimum soc -func PhasesHandler(lp loadpoint.API) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - phases, err := strconv.ParseInt(vars["value"], 10, 32) - if err == nil { - err = lp.SetPhases(int(phases)) - } - - if err != nil { - w.WriteHeader(http.StatusBadRequest) - jsonResponse(w, r, errorJSON{Error: err.Error()}) - return - } - - jsonResponse(w, r, map[string]interface{}{result: lp.GetPhases()}) - } -} - -// RemoteDemandHandler updates minimum soc -func RemoteDemandHandler(lp loadpoint.API) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - demandS, ok := vars["demand"] - - var source string - if ok { - source, ok = vars["source"] - } - - demand, err := loadpoint.RemoteDemandString(demandS) - - if !ok || err != nil { - w.WriteHeader(http.StatusBadRequest) - jsonResponse(w, r, errorJSON{Error: err.Error()}) - return - } - - lp.RemoteControl(source, demand) - - res := struct { - Demand loadpoint.RemoteDemand `json:"demand"` - Source string `json:"source"` - }{ - Source: source, - Demand: demand, - } - - jsonResponse(w, r, res) - } -} - -func timezone() *time.Location { - tz := os.Getenv("TZ") - if tz == "" { - tz = "Local" - } - - loc, _ := time.LoadLocation(tz) - return loc -} - -// TargetChargeHandler updates target soc -func TargetChargeHandler(loadpoint loadpoint.API) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - - socS, ok := vars["soc"] - socV, err := strconv.ParseInt(socS, 10, 32) - - if !ok || err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - - timeS, ok := vars["time"] - timeV, err := time.ParseInLocation("2006-01-02T15:04:05", timeS, timezone()) - - if !ok || err != nil { - w.WriteHeader(http.StatusBadRequest) - jsonResponse(w, r, errorJSON{Error: err.Error()}) - return - } - - loadpoint.SetTargetCharge(timeV, int(socV)) - - res := struct { - SoC int64 `json:"soc"` - Time time.Time `json:"time"` - }{ - SoC: socV, - Time: timeV, - } - - jsonResponse(w, r, res) - } -} - -// SocketHandler attaches websocket handler to uri -func SocketHandler(hub *SocketHub) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ServeWebsocket(hub, w, r) - } -} - // HTTPd wraps an http.Server and adds the root router type HTTPd struct { *http.Server @@ -350,15 +43,14 @@ type HTTPd struct { // NewHTTPd creates HTTP server with configured routes for loadpoint func NewHTTPd(url string, site site.API, hub *SocketHub, cache *util.Cache) *HTTPd { routes := map[string]route{ - "health": {[]string{"GET"}, "/health", HealthHandler(site)}, - "state": {[]string{"GET"}, "/state", StateHandler(cache)}, - "templates": {[]string{"GET"}, "/config/templates/{class:[a-z]+}", TemplatesHandler()}, + "health": {[]string{"GET"}, "/health", healthHandler(site)}, + "state": {[]string{"GET"}, "/state", stateHandler(cache)}, } router := mux.NewRouter().StrictSlash(true) // websocket - router.HandleFunc("/ws", SocketHandler(hub)) + router.HandleFunc("/ws", socketHandler(hub)) // static - individual handlers per root and folders static := router.PathPrefix("/").Subrouter() @@ -389,14 +81,14 @@ func NewHTTPd(url string, site site.API, hub *SocketHub, cache *util.Cache) *HTT lpAPI := api.PathPrefix(fmt.Sprintf("/loadpoints/%d", id)).Subrouter() routes := map[string]route{ - "mode": {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", ChargeModeHandler(lp)}, - "targetsoc": {[]string{"POST", "OPTIONS"}, "/targetsoc/{value:[0-9]+}", TargetSoCHandler(lp)}, - "minsoc": {[]string{"POST", "OPTIONS"}, "/minsoc/{value:[0-9]+}", MinSoCHandler(lp)}, - "mincurrent": {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9]+}", MinCurrentHandler(lp)}, - "maxcurrent": {[]string{"POST", "OPTIONS"}, "/maxcurrent/{value:[0-9]+}", MaxCurrentHandler(lp)}, - "phases": {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", PhasesHandler(lp)}, - "targetcharge": {[]string{"POST", "OPTIONS"}, "/targetcharge/{soc:[0-9]+}/{time:[0-9TZ:-]+}", TargetChargeHandler(lp)}, - "remotedemand": {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source::[0-9a-zA-Z_-]+}", RemoteDemandHandler(lp)}, + "mode": {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", chargeModeHandler(lp)}, + "targetsoc": {[]string{"POST", "OPTIONS"}, "/targetsoc/{value:[0-9]+}", targetSoCHandler(lp)}, + "minsoc": {[]string{"POST", "OPTIONS"}, "/minsoc/{value:[0-9]+}", minSoCHandler(lp)}, + "mincurrent": {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9]+}", minCurrentHandler(lp)}, + "maxcurrent": {[]string{"POST", "OPTIONS"}, "/maxcurrent/{value:[0-9]+}", maxCurrentHandler(lp)}, + "phases": {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", phasesHandler(lp)}, + "targetcharge": {[]string{"POST", "OPTIONS"}, "/targetcharge/{soc:[0-9]+}/{time:[0-9TZ:-]+}", targetChargeHandler(lp)}, + "remotedemand": {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source::[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)}, } for _, r := range routes { diff --git a/server/http_handler.go b/server/http_handler.go new file mode 100644 index 000000000..217483668 --- /dev/null +++ b/server/http_handler.go @@ -0,0 +1,288 @@ +package server + +import ( + "encoding/json" + "fmt" + "io/fs" + "net/http" + "os" + "strconv" + "text/template" + "time" + + "github.com/evcc-io/evcc/api" + "github.com/evcc-io/evcc/core/loadpoint" + "github.com/evcc-io/evcc/core/site" + "github.com/evcc-io/evcc/util" + "github.com/gorilla/mux" +) + +const ( + result = "result" + failure = "error" +) + +func indexHandler(site site.API) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=UTF-8") + + indexTemplate, err := fs.ReadFile(Assets, "index.html") + if err != nil { + log.FATAL.Print("httpd: failed to load embedded template:", err.Error()) + log.FATAL.Fatal("Make sure templates are included using the `release` build tag or use `make build`") + } + + t, err := template.New("evcc").Delims("[[", "]]").Parse(string(indexTemplate)) + if err != nil { + log.FATAL.Fatal("httpd: failed to create main page template:", err.Error()) + } + + if err := t.Execute(w, map[string]interface{}{ + "Version": Version, + "Commit": Commit, + "Configured": len(site.LoadPoints()), + }); err != nil { + log.ERROR.Println("httpd: failed to render main page:", err.Error()) + } + }) +} + +// jsonHandler is a middleware that decorates responses with JSON and CORS headers +func jsonHandler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + h.ServeHTTP(w, r) + }) +} + +func jsonResponse(w http.ResponseWriter, r *http.Request, content interface{}) { + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(content); err != nil { + log.ERROR.Printf("httpd: failed to encode JSON: %v", err) + } +} + +// healthHandler returns current charge mode +func healthHandler(site site.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !site.Healthy() { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "OK") + } +} + +// stateHandler returns current charge mode +func stateHandler(cache *util.Cache) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + res := cache.State() + for _, k := range []string{"availableVersion", "releaseNotes"} { + delete(res, k) + } + jsonResponse(w, r, res) + } +} + +// chargeModeHandler updates charge mode +func chargeModeHandler(lp loadpoint.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + mode, err := api.ChargeModeString(vars["value"]) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + jsonResponse(w, r, map[string]interface{}{failure: err.Error()}) + return + } + + lp.SetMode(mode) + + jsonResponse(w, r, map[string]interface{}{result: lp.GetMode()}) + } +} + +// targetSoCHandler updates target soc +func targetSoCHandler(lp loadpoint.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + soc, err := strconv.ParseInt(vars["value"], 10, 32) + if err == nil { + err = lp.SetTargetSoC(int(soc)) + } + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + jsonResponse(w, r, map[string]interface{}{failure: err.Error()}) + return + } + + jsonResponse(w, r, map[string]interface{}{result: lp.GetTargetSoC()}) + } +} + +// minSoCHandler updates minimum soc +func minSoCHandler(lp loadpoint.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + soc, err := strconv.ParseInt(vars["value"], 10, 32) + if err == nil { + err = lp.SetMinSoC(int(soc)) + } + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + jsonResponse(w, r, map[string]interface{}{failure: err.Error()}) + return + } + + jsonResponse(w, r, map[string]interface{}{result: lp.GetMinSoC()}) + } +} + +// minCurrentHandler updates minimum current +func minCurrentHandler(lp loadpoint.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + current, err := strconv.ParseFloat(vars["value"], 64) + if err == nil { + lp.SetMinCurrent(current) + } else { + w.WriteHeader(http.StatusBadRequest) + jsonResponse(w, r, map[string]interface{}{failure: err.Error()}) + return + } + + jsonResponse(w, r, map[string]interface{}{result: lp.GetMinCurrent()}) + } +} + +// maxCurrentHandler updates maximum current +func maxCurrentHandler(lp loadpoint.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + current, err := strconv.ParseFloat(vars["value"], 64) + if err == nil { + lp.SetMaxCurrent(current) + } else { + w.WriteHeader(http.StatusBadRequest) + jsonResponse(w, r, map[string]interface{}{failure: err.Error()}) + return + } + + jsonResponse(w, r, map[string]interface{}{result: lp.GetMaxCurrent()}) + } +} + +// phasesHandler updates minimum soc +func phasesHandler(lp loadpoint.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + phases, err := strconv.ParseInt(vars["value"], 10, 32) + if err == nil { + err = lp.SetPhases(int(phases)) + } + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + jsonResponse(w, r, map[string]interface{}{failure: err.Error()}) + return + } + + jsonResponse(w, r, map[string]interface{}{result: lp.GetPhases()}) + } +} + +// remoteDemandHandler updates minimum soc +func remoteDemandHandler(lp loadpoint.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + demandS, ok := vars["demand"] + + var source string + if ok { + source, ok = vars["source"] + } + + demand, err := loadpoint.RemoteDemandString(demandS) + + if !ok || err != nil { + w.WriteHeader(http.StatusBadRequest) + jsonResponse(w, r, map[string]interface{}{failure: err.Error()}) + return + } + + lp.RemoteControl(source, demand) + + res := struct { + Demand loadpoint.RemoteDemand `json:"demand"` + Source string `json:"source"` + }{ + Source: source, + Demand: demand, + } + + jsonResponse(w, r, res) + } +} + +func timezone() *time.Location { + tz := os.Getenv("TZ") + if tz == "" { + tz = "Local" + } + + loc, _ := time.LoadLocation(tz) + return loc +} + +// targetChargeHandler updates target soc +func targetChargeHandler(loadpoint loadpoint.API) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + socS, ok := vars["soc"] + socV, err := strconv.ParseInt(socS, 10, 32) + + if !ok || err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + timeS, ok := vars["time"] + timeV, err := time.ParseInLocation("2006-01-02T15:04:05", timeS, timezone()) + + if !ok || err != nil { + w.WriteHeader(http.StatusBadRequest) + jsonResponse(w, r, map[string]interface{}{failure: err.Error()}) + return + } + + loadpoint.SetTargetCharge(timeV, int(socV)) + + res := struct { + SoC int64 `json:"soc"` + Time time.Time `json:"time"` + }{ + SoC: socV, + Time: timeV, + } + + jsonResponse(w, r, res) + } +} + +// socketHandler attaches websocket handler to uri +func socketHandler(hub *SocketHub) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ServeWebsocket(hub, w, r) + } +} diff --git a/server/uds.go b/server/uds.go index 5d50c97d9..43e30d64a 100644 --- a/server/uds.go +++ b/server/uds.go @@ -38,7 +38,7 @@ func HealthListener(site site.API) { mux := http.NewServeMux() httpd := http.Server{Handler: mux} - mux.HandleFunc("/health", HealthHandler(site)) + mux.HandleFunc("/health", healthHandler(site)) _ = httpd.Serve(l) }