package cmd import ( "fmt" "net" "net/http" "strconv" "strings" "time" "github.com/dustin/go-humanize" "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/charger" "github.com/evcc-io/evcc/meter" "github.com/evcc-io/evcc/provider/mqtt" "github.com/evcc-io/evcc/push" "github.com/evcc-io/evcc/server" autoauth "github.com/evcc-io/evcc/server/auth" "github.com/evcc-io/evcc/util" "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 interface{} // TODO deprecated Network networkConfig Log string SponsorToken string Metrics bool Profile bool Levels map[string]string Interval time.Duration Mqtt mqttConfig Javascript map[string]interface{} Influx server.InfluxConfig EEBus map[string]interface{} HEMS typedConfig Messaging messagingConfig Meters []qualifiedConfig Chargers []qualifiedConfig Vehicles []qualifiedConfig Tariffs tariffConfig Site map[string]interface{} 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 } func (conf *mqttConfig) RootTopic() string { if conf.Topic != "" { return conf.Topic } return "evcc" } type qualifiedConfig struct { Name, Type string Other map[string]interface{} `mapstructure:",remain"` } type typedConfig struct { Type string Other map[string]interface{} `mapstructure:",remain"` } type messagingConfig struct { Events map[string]push.EventTemplateConfig Services []typedConfig } type tariffConfig struct { Currency string Grid typedConfig FeedIn typedConfig } // ConfigProvider provides configuration items type ConfigProvider struct { meters map[string]api.Meter chargers map[string]api.Charger vehicles map[string]api.Vehicle visited map[string]bool auth *util.AuthCollection } func (cp *ConfigProvider) TrackVisitors() { cp.visited = make(map[string]bool) } // Meter provides meters by name func (cp *ConfigProvider) Meter(name string) api.Meter { if meter, ok := cp.meters[name]; ok { // track duplicate usage https://github.com/evcc-io/evcc/issues/1744 if cp.visited != nil { if _, ok := cp.visited[name]; ok { log.FATAL.Fatalf("duplicate meter usage: %s", name) } cp.visited[name] = true } return meter } log.FATAL.Fatalf("invalid meter: %s", name) return nil } // Charger provides chargers by name func (cp *ConfigProvider) Charger(name string) api.Charger { if charger, ok := cp.chargers[name]; ok { return charger } log.FATAL.Fatalf("invalid charger: %s", name) return nil } // Vehicle provides vehicles by name func (cp *ConfigProvider) Vehicle(name string) api.Vehicle { if vehicle, ok := cp.vehicles[name]; ok { return vehicle } log.FATAL.Fatalf("invalid vehicle: %s", name) return nil } func (cp *ConfigProvider) configure(conf config) error { err := cp.configureMeters(conf) if err == nil { err = cp.configureChargers(conf) } if err == nil { err = cp.configureVehicles(conf) } return err } func (cp *ConfigProvider) configureMeters(conf config) error { cp.meters = make(map[string]api.Meter) for id, cc := range conf.Meters { if cc.Name == "" { return fmt.Errorf("cannot create %s meter: missing name", humanize.Ordinal(id+1)) } m, err := meter.NewFromConfig(cc.Type, cc.Other) if err != nil { err = fmt.Errorf("cannot create meter '%s': %w", cc.Name, err) return err } if _, exists := cp.meters[cc.Name]; exists { return fmt.Errorf("duplicate meter name: %s already defined and must be unique", cc.Name) } cp.meters[cc.Name] = m } return nil } func (cp *ConfigProvider) configureChargers(conf config) error { cp.chargers = make(map[string]api.Charger) for id, cc := range conf.Chargers { if cc.Name == "" { return fmt.Errorf("cannot create %s charger: missing name", humanize.Ordinal(id+1)) } c, err := charger.NewFromConfig(cc.Type, cc.Other) if err != nil { err = fmt.Errorf("cannot create charger '%s': %w", cc.Name, err) return err } if _, exists := cp.chargers[cc.Name]; exists { return fmt.Errorf("duplicate charger name: %s already defined and must be unique", cc.Name) } cp.chargers[cc.Name] = c } return nil } func (cp *ConfigProvider) configureVehicles(conf config) error { cp.vehicles = make(map[string]api.Vehicle) for id, cc := range conf.Vehicles { if cc.Name == "" { return fmt.Errorf("cannot create %s vehicle: missing name", humanize.Ordinal(id+1)) } // ensure vehicle config has title ccWithTitle := struct { Title string Other map[string]interface{} `mapstructure:",remain"` }{} if err := util.DecodeOther(cc.Other, &ccWithTitle); err != nil { return err } if ccWithTitle.Title == "" { //lint:ignore SA1019 as Title is safe on ascii cc.Other["title"] = strings.Title(cc.Name) } v, err := vehicle.NewFromConfig(cc.Type, cc.Other) if err != nil { // wrap any created errors to prevent fatals v, _ = wrapper.New(v, err) } if _, exists := cp.vehicles[cc.Name]; exists { return fmt.Errorf("duplicate vehicle name: %s already defined and must be unique", cc.Name) } cp.vehicles[cc.Name] = v } return nil } // webControl handles routing for devices. For now only api.ProviderLogin related routes 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( handlers.AllowedHeaders([]string{"Content-Type"}), )) // wire the handler autoauth.Setup(auth) // initialize cp.auth = util.NewAuthCollection(paramC) baseURI := conf.URI() baseAuthURI := fmt.Sprintf("%s/oauth", baseURI) var id int for _, v := range cp.vehicles { if provider, ok := v.(api.ProviderLogin); ok { id += 1 basePath := fmt.Sprintf("vehicles/%d", id) callbackURI := fmt.Sprintf("%s/%s/callback", baseAuthURI, basePath) // register vehicle ap := cp.auth.Register(fmt.Sprintf("oauth/%s", basePath), v.Title()) provider.SetCallbackParams(baseURI, callbackURI, ap.Handler()) auth. Methods(http.MethodPost). Path(fmt.Sprintf("/%s/login", basePath)). HandlerFunc(provider.LoginHandler()) auth. Methods(http.MethodPost). Path(fmt.Sprintf("/%s/logout", basePath)). HandlerFunc(provider.LogoutHandler()) log.INFO.Printf("ensure the oauth client redirect/callback is configured for %s: %s", v.Title(), callbackURI) } } cp.auth.Publish() }