286 lines
6.9 KiB
Go
286 lines
6.9 KiB
Go
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 {
|
|
if c.Schema == "http" && c.Port == 80 || c.Schema == "https" && c.Port == 443 {
|
|
return c.Host
|
|
}
|
|
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
|
|
var 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.AuthProvider 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.AuthProvider); 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()
|
|
}
|