evcc-io/cmd/config.go

284 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
Telemetry bool
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, error) {
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, nil
}
return nil, fmt.Errorf("meter does not exist: %s", name)
}
// Charger provides chargers by name
func (cp *ConfigProvider) Charger(name string) (api.Charger, error) {
if charger, ok := cp.chargers[name]; ok {
return charger, nil
}
return nil, fmt.Errorf("charger does not exist: %s", name)
}
// Vehicle provides vehicles by name
func (cp *ConfigProvider) Vehicle(name string) (api.Vehicle, error) {
if vehicle, ok := cp.vehicles[name]; ok {
return vehicle, nil
}
return nil, fmt.Errorf("vehicle does not exist: %s", name)
}
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()
}