From 1a4da3f7cada077410fd7a17cae44a28a2915327 Mon Sep 17 00:00:00 2001 From: andig Date: Thu, 14 Apr 2022 13:24:46 +0200 Subject: [PATCH] Improve network configuration (#3155) --- cmd/charger.go | 3 +-- cmd/config.go | 35 ++++++++++++++++--------- cmd/configure/configure.tpl | 19 +++++++++----- cmd/demo.go | 8 ++---- cmd/demo.yaml | 3 +++ cmd/dump.go | 3 +-- cmd/meter.go | 3 +-- cmd/root.go | 51 ++++++++++++++++++++++++------------- cmd/setup.go | 4 +-- cmd/token.go | 4 +-- cmd/token_tronity.go | 10 ++------ cmd/vehicle.go | 3 +-- core/site.go | 6 ++--- evcc.dist.yaml | 13 +++++++++- server/http.go | 4 +-- vehicle/mercedes.go | 6 +++-- 16 files changed, 105 insertions(+), 70 deletions(-) diff --git a/cmd/charger.go b/cmd/charger.go index 4c6d66670..660bcae8b 100644 --- a/cmd/charger.go +++ b/cmd/charger.go @@ -38,8 +38,7 @@ func runCharger(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s", server.FormattedVersion()) // load config - conf, err := loadConfigFile(cfgFile) - if err != nil { + if err := loadConfigFile(cfgFile, &conf); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/config.go b/cmd/config.go index a7a6a150f..779ae0c4d 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -2,7 +2,9 @@ package cmd import ( "fmt" + "net" "net/http" + "strconv" "strings" "time" @@ -18,10 +20,12 @@ import ( "github.com/evcc-io/evcc/vehicle" "github.com/evcc-io/evcc/vehicle/wrapper" "github.com/gorilla/handlers" + "github.com/gorilla/mux" ) type config struct { - URI string + URI interface{} // TODO deprecated + Network networkConfig Log string SponsorToken string Metrics bool @@ -42,6 +46,20 @@ type config struct { LoadPoints []map[string]interface{} } +type networkConfig struct { + Schema string + Host string + Port int +} + +func (c networkConfig) HostPort() string { + return net.JoinHostPort(c.Host, strconv.Itoa(c.Port)) +} + +func (c networkConfig) URI() string { + return fmt.Sprintf("%s://%s", c.Schema, c.HostPort()) +} + type mqttConfig struct { mqtt.Config `mapstructure:",squash"` Topic string @@ -218,9 +236,7 @@ func (cp *ConfigProvider) configureVehicles(conf config) error { } // webControl handles routing for devices. For now only api.ProviderLogin related routes -func (cp *ConfigProvider) webControl(httpd *server.HTTPd, paramC chan<- util.Param) { - router := httpd.Router() - +func (cp *ConfigProvider) webControl(conf networkConfig, router *mux.Router, paramC chan<- util.Param) { auth := router.PathPrefix("/oauth").Subrouter() auth.Use(handlers.CompressHandler) auth.Use(handlers.CORS( @@ -233,9 +249,8 @@ func (cp *ConfigProvider) webControl(httpd *server.HTTPd, paramC chan<- util.Par // initialize cp.auth = util.NewAuthCollection(paramC) - // TODO make evccURI configurable, add warnings for any network/ localhost - evccURI := fmt.Sprintf("http://%s", httpd.Addr) - baseAuthURI := fmt.Sprintf("%s/oauth", evccURI) + baseURI := conf.URI() + baseAuthURI := fmt.Sprintf("%s/oauth", baseURI) var id int for _, v := range cp.vehicles { @@ -245,14 +260,10 @@ func (cp *ConfigProvider) webControl(httpd *server.HTTPd, paramC chan<- util.Par basePath := fmt.Sprintf("vehicles/%d", id) callbackURI := fmt.Sprintf("%s/%s/callback", baseAuthURI, basePath) - // replace interface designator with address - // TODO fix when evccURI becomes configurable - callbackURI = strings.ReplaceAll(callbackURI, "0.0.0.0", "localhost") - // register vehicle ap := cp.auth.Register(fmt.Sprintf("oauth/%s", basePath), v.Title()) - provider.SetCallbackParams(evccURI, callbackURI, ap.Handler()) + provider.SetCallbackParams(baseURI, callbackURI, ap.Handler()) auth. Methods(http.MethodPost). diff --git a/cmd/configure/configure.tpl b/cmd/configure/configure.tpl index 68c3e1401..1c02662ab 100644 --- a/cmd/configure/configure.tpl +++ b/cmd/configure/configure.tpl @@ -1,7 +1,18 @@ -uri: 0.0.0.0:7070 # uri for ui -interval: 10s # control cycle interval +# open evcc at http://evcc.local:7070 +network: + schema: http + host: evcc.local # .local suffix announces the hostname on MDNS + port: 7070 log: info +levels: + cache: error + +interval: 10s # control cycle interval +{{- if ne (len .SponsorToken) 0 }} + +sponsortoken: {{ .SponsorToken }} +{{- end}} {{- if ne (len .Meters) 0 }} meters: @@ -74,7 +85,3 @@ hems: eebus: {{ .EEBUS | indent 2 }} {{- end }} -{{- if ne (len .SponsorToken) 0 }} - -sponsortoken: {{ .SponsorToken }} -{{- end}} diff --git a/cmd/demo.go b/cmd/demo.go index 9b12afdd8..bcf7e09fd 100644 --- a/cmd/demo.go +++ b/cmd/demo.go @@ -2,7 +2,6 @@ package cmd import ( _ "embed" // for yaml - "fmt" "github.com/spf13/viper" "gopkg.in/yaml.v3" @@ -11,7 +10,7 @@ import ( //go:embed demo.yaml var demoYaml string -func demoConfig() (conf config) { +func demoConfig(conf *config) { demo := map[string]interface{}{} if err := yaml.Unmarshal([]byte(demoYaml), &demo); err != nil { @@ -22,12 +21,9 @@ func demoConfig() (conf config) { viper.Set(k, v) } - // demo port - viper.Set("uri", fmt.Sprintf("0.0.0.0:%d", defaultPort)) - if err := viper.UnmarshalExact(&conf); err != nil { log.FATAL.Fatalf("failed loading demo config: %v", err) } - return conf + conf.Network.Port = defaultPort } diff --git a/cmd/demo.yaml b/cmd/demo.yaml index 68748609f..37d914491 100644 --- a/cmd/demo.yaml +++ b/cmd/demo.yaml @@ -1,3 +1,6 @@ +network: + port: 7070 + log: info interval: 3s diff --git a/cmd/dump.go b/cmd/dump.go index 866d933e7..5895aaa5b 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -26,8 +26,7 @@ func runDump(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s", server.FormattedVersion()) // load config - conf, err := loadConfigFile(cfgFile) - if err != nil { + if err := loadConfigFile(cfgFile, &conf); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/meter.go b/cmd/meter.go index 3440484ca..57b7ded1c 100644 --- a/cmd/meter.go +++ b/cmd/meter.go @@ -29,8 +29,7 @@ func runMeter(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s", server.FormattedVersion()) // load config - conf, err := loadConfigFile(cfgFile) - if err != nil { + if err := loadConfigFile(cfgFile, &conf); err != nil { log.FATAL.Fatal(err) } diff --git a/cmd/root.go b/cmd/root.go index d67cd3915..d38dde77b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,12 +2,11 @@ package cmd import ( "fmt" - "net" "net/http" _ "net/http/pprof" // pprof handler "os" "os/signal" - "strconv" + "strings" "sync" "syscall" "time" @@ -34,6 +33,14 @@ var ( ignoreMqtt = []string{"auth", "releaseNotes"} // excessive size may crash certain brokers ) +var conf = config{ + Network: networkConfig{ + Schema: "http", + Host: "evcc.local", + Port: 7070, + }, +} + // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "evcc", @@ -72,8 +79,10 @@ func init() { cobra.OnInitialize(initConfig) configureCommand(rootCmd) - rootCmd.PersistentFlags().StringP("uri", "u", "0.0.0.0:7070", "Listen address") - bind(rootCmd, "uri") + rootCmd.PersistentFlags().IntP("port", "p", 7070, "Listen port") + if err := viper.BindPFlag("network.port", rootCmd.PersistentFlags().Lookup("port")); err != nil { + panic(err) + } rootCmd.PersistentFlags().DurationP("interval", "i", 10*time.Second, "Update interval") bind(rootCmd, "interval") @@ -134,16 +143,23 @@ func run(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s", server.FormattedVersion()) // load config and re-configure logging after reading config file - conf, err := loadConfigFile(cfgFile) - if err != nil { + if err := loadConfigFile(cfgFile, &conf); err != nil { log.ERROR.Println("missing evcc config - switching into demo mode") - conf = demoConfig() + demoConfig(&conf) } util.LogLevel(viper.GetString("log"), viper.GetStringMapString("levels")) - uri := viper.GetString("uri") - log.INFO.Println("listening at", uri) + // network config + if viper.GetString("uri") != "" { + log.ERROR.Println("`uri` is deprecated and will be ignored. Use `network` instead.") + } + + if cmd.PersistentFlags().Lookup("port").Changed { + conf.Network.Port = viper.GetInt("network.port") + } + + log.INFO.Printf("listening at :%d", conf.Network.Port) // setup environment if err := configureEnvironment(conf); err != nil { @@ -183,16 +199,15 @@ func run(cmd *cobra.Command, args []string) { // create webserver socketHub := server.NewSocketHub() - httpd := server.NewHTTPd(uri, site, socketHub, cache) + httpd := server.NewHTTPd(fmt.Sprintf(":%d", conf.Network.Port), site, socketHub, cache) // announce webserver on mDNS - if _, port, err := net.SplitHostPort(uri); err == nil { - if portInt, err := strconv.Atoi(port); err == nil { - if zc, err := zeroconf.RegisterProxy("evcc Website", "_http._tcp", "local.", portInt, "evcc", nil, []string{}, nil); err == nil { - shutdown.Register(zc.Shutdown) - } else { - log.ERROR.Printf("mDNS announcement: %s", err) - } + if strings.HasSuffix(conf.Network.Host, ".local") { + host := strings.TrimSuffix(conf.Network.Host, ".local") + if zc, err := zeroconf.RegisterProxy("EV Charge Controller", "_http._tcp", "local.", conf.Network.Port, host, nil, []string{}, nil); err == nil { + shutdown.Register(zc.Shutdown) + } else { + log.ERROR.Printf("mDNS announcement: %s", err) } } @@ -225,7 +240,7 @@ func run(cmd *cobra.Command, args []string) { } // allow web access for vehicles - cp.webControl(httpd, valueChan) + cp.webControl(conf.Network, httpd.Router(), valueChan) // version check go updater.Run(log, httpd, tee, valueChan) diff --git a/cmd/setup.go b/cmd/setup.go index 7b0abb7b5..70e772e5a 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -31,7 +31,7 @@ func init() { var cp = new(ConfigProvider) -func loadConfigFile(cfgFile string) (conf config, err error) { +func loadConfigFile(cfgFile string, conf *config) (err error) { if cfgFile != "" { log.INFO.Println("using config file", cfgFile) if err := viper.UnmarshalExact(&conf); err != nil { @@ -41,7 +41,7 @@ func loadConfigFile(cfgFile string) (conf config, err error) { err = errors.New("missing evcc config") } - return conf, err + return err } func configureEnvironment(conf config) (err error) { diff --git a/cmd/token.go b/cmd/token.go index b240320c6..b7d7efe3a 100644 --- a/cmd/token.go +++ b/cmd/token.go @@ -29,8 +29,7 @@ func runToken(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s", server.FormattedVersion()) // load config - conf, err := loadConfigFile(cfgFile) - if err != nil { + if err := loadConfigFile(cfgFile, &conf); err != nil { log.FATAL.Fatal(err) } @@ -55,6 +54,7 @@ func runToken(cmd *cobra.Command, args []string) { } var token *oauth2.Token + var err error switch strings.ToLower(vehicleConf.Type) { case "tesla": diff --git a/cmd/token_tronity.go b/cmd/token_tronity.go index a94efd652..e83cd84c9 100644 --- a/cmd/token_tronity.go +++ b/cmd/token_tronity.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "io" - "net" "net/http" "strings" "sync" @@ -133,13 +132,8 @@ func tronityToken(conf config, vehicleConf qualifiedConfig) (*oauth2.Token, erro } if oc.RedirectURL = cc.RedirectURI; oc.RedirectURL == "" { - _, port, err := net.SplitHostPort(conf.URI) - if err != nil { - return nil, err - } - - oc.RedirectURL = fmt.Sprintf("http://%s/auth/tronity", net.JoinHostPort("localhost", port)) + oc.RedirectURL = fmt.Sprintf("%s/auth/tronity", conf.Network.URI()) } - return tronityAuthorize(conf.URI, oc) + return tronityAuthorize(conf.Network.HostPort(), oc) } diff --git a/cmd/vehicle.go b/cmd/vehicle.go index f21e460e1..ae3f470de 100644 --- a/cmd/vehicle.go +++ b/cmd/vehicle.go @@ -32,8 +32,7 @@ func runVehicle(cmd *cobra.Command, args []string) { log.INFO.Printf("evcc %s", server.FormattedVersion()) // load config - conf, err := loadConfigFile(cfgFile) - if err != nil { + if err := loadConfigFile(cfgFile, &conf); err != nil { log.FATAL.Fatal(err) } diff --git a/core/site.go b/core/site.go index 8adfca124..cd5f0ca23 100644 --- a/core/site.go +++ b/core/site.go @@ -172,7 +172,7 @@ func (site *Site) DumpConfig() { if len(site.pvMeters) > 0 { for i, pv := range site.pvMeters { - site.log.INFO.Println(meterCapabilities(fmt.Sprintf("pv %d", i), pv)) + site.log.INFO.Println(meterCapabilities(fmt.Sprintf("pv %d", i+1), pv)) } } @@ -180,7 +180,7 @@ func (site *Site) DumpConfig() { for i, battery := range site.batteryMeters { _, ok := battery.(api.Battery) site.log.INFO.Println( - meterCapabilities(fmt.Sprintf("battery %d", i), battery), + meterCapabilities(fmt.Sprintf("battery %d", i+1), battery), fmt.Sprintf("soc %s", presence[ok]), ) } @@ -217,7 +217,7 @@ func (site *Site) DumpConfig() { _, status := v.(api.ChargeState) _, climate := v.(api.VehicleClimater) lp.log.INFO.Printf(" vehicle %d: range %s finish %s status %s climate %s", - i, presence[rng], presence[finish], presence[status], presence[climate], + i+1, presence[rng], presence[finish], presence[status], presence[climate], ) } } diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 252d3b09d..c304412e5 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -1,4 +1,15 @@ -uri: 0.0.0.0:7070 # uri for ui +network: + # schema is the HTTP schema + # setting to `https` does not enable https, it only changes the way URLs are generated + schema: http + # host is the hostname or IP address + # if the host name contains a `.local` suffix, the name will be announced on MDNS + # docker: MDNS announcements don't work. host must be set to the docker host's name. + host: evcc.local + # port is the listening port for UI and api + # evcc will listen on all available interfaces + port: 7070 + interval: 10s # control cycle interval # sponsor token enables optional features (request at https://cloud.evcc.io) diff --git a/server/http.go b/server/http.go index 85e34bba2..3b5ef71ac 100644 --- a/server/http.go +++ b/server/http.go @@ -41,7 +41,7 @@ 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 { +func NewHTTPd(addr 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)}, @@ -98,7 +98,7 @@ func NewHTTPd(url string, site site.API, hub *SocketHub, cache *util.Cache) *HTT srv := &HTTPd{ Server: &http.Server{ - Addr: url, + Addr: addr, Handler: router, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, diff --git a/vehicle/mercedes.go b/vehicle/mercedes.go index 73610b73c..4c276783d 100644 --- a/vehicle/mercedes.go +++ b/vehicle/mercedes.go @@ -13,6 +13,7 @@ import ( // Mercedes is an api.Vehicle implementation for Mercedes cars type Mercedes struct { *embed + api.ProviderLogin *mercedes.Provider } @@ -63,8 +64,9 @@ func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) { api := mercedes.NewAPI(log, identity, cc.Sandbox) v := &Mercedes{ - embed: &cc.embed, - Provider: mercedes.NewProvider(api, strings.ToUpper(cc.VIN), cc.Cache), + embed: &cc.embed, + Provider: mercedes.NewProvider(api, strings.ToUpper(cc.VIN), cc.Cache), + ProviderLogin: identity, // expose the OAuth2 login } return v, nil