930 lines
22 KiB
Go
930 lines
22 KiB
Go
package cmd
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
paho "github.com/eclipse/paho.mqtt.golang"
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/charger"
|
|
"github.com/evcc-io/evcc/charger/eebus"
|
|
"github.com/evcc-io/evcc/cmd/shutdown"
|
|
"github.com/evcc-io/evcc/core"
|
|
"github.com/evcc-io/evcc/core/site"
|
|
"github.com/evcc-io/evcc/hems"
|
|
"github.com/evcc-io/evcc/meter"
|
|
"github.com/evcc-io/evcc/provider/golang"
|
|
"github.com/evcc-io/evcc/provider/javascript"
|
|
"github.com/evcc-io/evcc/provider/mqtt"
|
|
"github.com/evcc-io/evcc/push"
|
|
"github.com/evcc-io/evcc/server"
|
|
"github.com/evcc-io/evcc/server/db"
|
|
"github.com/evcc-io/evcc/server/db/settings"
|
|
"github.com/evcc-io/evcc/server/oauth2redirect"
|
|
"github.com/evcc-io/evcc/tariff"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/config"
|
|
"github.com/evcc-io/evcc/util/locale"
|
|
"github.com/evcc-io/evcc/util/machine"
|
|
"github.com/evcc-io/evcc/util/modbus"
|
|
"github.com/evcc-io/evcc/util/pipe"
|
|
"github.com/evcc-io/evcc/util/request"
|
|
"github.com/evcc-io/evcc/util/sponsor"
|
|
"github.com/evcc-io/evcc/util/templates"
|
|
"github.com/evcc-io/evcc/vehicle"
|
|
"github.com/gorilla/handlers"
|
|
"github.com/gorilla/mux"
|
|
"github.com/libp2p/zeroconf/v2"
|
|
"github.com/spf13/cast"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
"golang.org/x/sync/errgroup"
|
|
"golang.org/x/text/currency"
|
|
)
|
|
|
|
var conf = globalConfig{
|
|
Interval: 10 * time.Second,
|
|
Log: "info",
|
|
Network: networkConfig{
|
|
Schema: "http",
|
|
Host: "evcc.local",
|
|
Port: 7070,
|
|
},
|
|
Mqtt: mqttConfig{
|
|
Topic: "evcc",
|
|
},
|
|
Database: dbConfig{
|
|
Type: "sqlite",
|
|
Dsn: "~/.evcc/evcc.db",
|
|
},
|
|
}
|
|
|
|
var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`)
|
|
|
|
type globalConfig struct {
|
|
Network networkConfig
|
|
Log string
|
|
SponsorToken string
|
|
Plant string // telemetry plant id
|
|
Telemetry bool
|
|
Metrics bool
|
|
Profile bool
|
|
Levels map[string]string
|
|
Interval time.Duration
|
|
Database dbConfig
|
|
Mqtt mqttConfig
|
|
ModbusProxy []proxyConfig
|
|
Javascript []javascriptConfig
|
|
Go []goConfig
|
|
Influx server.InfluxConfig
|
|
EEBus map[string]interface{}
|
|
HEMS config.Typed
|
|
Messaging messagingConfig
|
|
Meters []config.Named
|
|
Chargers []config.Named
|
|
Vehicles []config.Named
|
|
Tariffs tariffConfig
|
|
Site map[string]interface{}
|
|
Loadpoints []map[string]interface{}
|
|
Circuits []config.Named
|
|
}
|
|
|
|
type mqttConfig struct {
|
|
mqtt.Config `mapstructure:",squash"`
|
|
Topic string
|
|
}
|
|
|
|
type javascriptConfig struct {
|
|
VM string
|
|
Script string
|
|
}
|
|
|
|
type goConfig struct {
|
|
VM string
|
|
Script string
|
|
}
|
|
|
|
type proxyConfig struct {
|
|
Port int
|
|
ReadOnly string
|
|
modbus.Settings `mapstructure:",squash"`
|
|
}
|
|
|
|
type dbConfig struct {
|
|
Type string
|
|
Dsn string
|
|
}
|
|
|
|
type messagingConfig struct {
|
|
Events map[string]push.EventTemplateConfig
|
|
Services []config.Typed
|
|
}
|
|
|
|
type tariffConfig struct {
|
|
Currency string
|
|
Grid config.Typed
|
|
FeedIn config.Typed
|
|
Co2 config.Typed
|
|
Planner config.Typed
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
func nameValid(name string) error {
|
|
if !nameRE.MatchString(name) {
|
|
return fmt.Errorf("name must not contain special characters or spaces: %s", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func tokenDanger(conf []config.Named) bool {
|
|
problematic := []string{"tesla", "psa", "opel", "citroen", "ds", "peugeot"}
|
|
|
|
for _, cc := range conf {
|
|
if slices.Contains(problematic, cc.Type) {
|
|
return true
|
|
}
|
|
template, ok := cc.Other["template"].(string)
|
|
if ok && cc.Type == "template" && slices.Contains(problematic, template) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func loadConfigFile(conf *globalConfig, checkDB bool) error {
|
|
err := viper.ReadInConfig()
|
|
|
|
if cfgFile = viper.ConfigFileUsed(); cfgFile == "" {
|
|
return err
|
|
}
|
|
|
|
log.INFO.Println("using config file:", cfgFile)
|
|
|
|
if err == nil {
|
|
if err = viper.UnmarshalExact(conf); err != nil {
|
|
err = fmt.Errorf("failed parsing config file: %w", err)
|
|
}
|
|
}
|
|
|
|
// check service database
|
|
if _, err := os.Stat(serviceDB); err == nil && checkDB && conf.Database.Dsn != serviceDB && tokenDanger(conf.Vehicles) {
|
|
log.FATAL.Fatal(`
|
|
|
|
Found systemd service database at "` + serviceDB + `", evcc has been invoked with database "` + conf.Database.Dsn + `".
|
|
Running evcc with vehicles configured in evcc.yaml may lead to expiring the yaml configuration's vehicle tokens.
|
|
This is due to the fact, that the token refresh will be saved to the local instead of the service's database.
|
|
If you have vehicles with touchy tokens like PSA or Tesla, make sure to remove vehicle configuration from the yaml file.
|
|
|
|
If you know what you're doing, you can run evcc ignoring the service database with the --ignore-db flag.
|
|
`)
|
|
}
|
|
|
|
// parse log levels after reading config
|
|
if err == nil {
|
|
parseLogLevels()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func configureCircuits(static []config.Named, names ...string) error {
|
|
children := slices.Clone(static)
|
|
|
|
// TODO: check for circular references
|
|
NEXT:
|
|
for i, cc := range children {
|
|
if cc.Name == "" {
|
|
return fmt.Errorf("cannot create circuit: missing name")
|
|
}
|
|
|
|
if err := nameValid(cc.Name); err != nil {
|
|
return fmt.Errorf("cannot create circuit: duplicate name: %s", cc.Name)
|
|
}
|
|
|
|
if parent := cast.ToString(cc.Property("parent")); parent != "" {
|
|
if _, err := config.Circuits().ByName(parent); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
log := util.NewLogger("circuit-" + cc.Name)
|
|
instance, err := core.NewCircuitFromConfig(log, cc.Other)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create circuit '%s': %w", cc.Name, err)
|
|
}
|
|
|
|
// ensure config has title
|
|
if instance.GetTitle() == "" {
|
|
//lint:ignore SA1019 as Title is safe on ascii
|
|
instance.SetTitle(strings.Title(cc.Name))
|
|
}
|
|
|
|
if err := config.Circuits().Add(config.NewStaticDevice(cc, instance)); err != nil {
|
|
return err
|
|
}
|
|
|
|
children = slices.Delete(children, i, i+1)
|
|
goto NEXT
|
|
}
|
|
|
|
if len(children) > 0 {
|
|
return fmt.Errorf("circuit is missing parent: %s", children[0].Name)
|
|
}
|
|
|
|
// append devices from database
|
|
configurable, err := config.ConfigurationsByClass(templates.Circuit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
children2 := slices.Clone(configurable)
|
|
|
|
NEXT2:
|
|
for i, conf := range children2 {
|
|
cc := conf.Named()
|
|
|
|
if len(names) > 0 && !slices.Contains(names, cc.Name) {
|
|
return nil
|
|
}
|
|
|
|
if parent := cast.ToString(cc.Property("parent")); parent != "" {
|
|
if _, err := config.Circuits().ByName(parent); err != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
log := util.NewLogger("circuit-" + cc.Name)
|
|
instance, err := core.NewCircuitFromConfig(log, cc.Other)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create circuit '%s': %w", cc.Name, err)
|
|
}
|
|
|
|
// ensure config has title
|
|
if instance.GetTitle() == "" {
|
|
//lint:ignore SA1019 as Title is safe on ascii
|
|
instance.SetTitle(strings.Title(cc.Name))
|
|
}
|
|
|
|
if err := config.Circuits().Add(config.NewConfigurableDevice(conf, instance)); err != nil {
|
|
return err
|
|
}
|
|
|
|
children2 = slices.Delete(children2, i, i+1)
|
|
goto NEXT2
|
|
}
|
|
|
|
if len(children2) > 0 {
|
|
return fmt.Errorf("missing parent circuit: %s", children2[0].Named().Name)
|
|
}
|
|
|
|
var rootFound bool
|
|
for _, dev := range config.Circuits().Devices() {
|
|
c := dev.Instance()
|
|
|
|
if c.GetParent() == nil {
|
|
if rootFound {
|
|
return errors.New("cannot have multiple root circuits")
|
|
}
|
|
rootFound = true
|
|
}
|
|
}
|
|
|
|
if !rootFound && len(config.Circuits().Devices()) > 0 {
|
|
return errors.New("root circuit required")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func configureMeters(static []config.Named, names ...string) error {
|
|
for i, cc := range static {
|
|
if cc.Name == "" {
|
|
return fmt.Errorf("cannot create meter %d: missing name", i+1)
|
|
}
|
|
|
|
if len(names) > 0 && !slices.Contains(names, cc.Name) {
|
|
continue
|
|
}
|
|
|
|
if err := nameValid(cc.Name); err != nil {
|
|
log.WARN.Printf("create meter %d: %v", i+1, err)
|
|
}
|
|
|
|
instance, err := meter.NewFromConfig(cc.Type, cc.Other)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)
|
|
}
|
|
|
|
if err := config.Meters().Add(config.NewStaticDevice(cc, instance)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// append devices from database
|
|
configurable, err := config.ConfigurationsByClass(templates.Meter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, conf := range configurable {
|
|
cc := conf.Named()
|
|
|
|
if len(names) > 0 && !slices.Contains(names, cc.Name) {
|
|
return nil
|
|
}
|
|
|
|
instance, err := meter.NewFromConfig(cc.Type, cc.Other)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create meter '%s': %w", cc.Name, err)
|
|
}
|
|
|
|
if err := config.Meters().Add(config.NewConfigurableDevice(conf, instance)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func configureChargers(static []config.Named, names ...string) error {
|
|
g, _ := errgroup.WithContext(context.Background())
|
|
|
|
for i, cc := range static {
|
|
if cc.Name == "" {
|
|
return fmt.Errorf("cannot create charger %d: missing name", i+1)
|
|
}
|
|
|
|
if len(names) > 0 && !slices.Contains(names, cc.Name) {
|
|
continue
|
|
}
|
|
|
|
if err := nameValid(cc.Name); err != nil {
|
|
log.WARN.Printf("create charger %d: %v", i+1, err)
|
|
}
|
|
|
|
g.Go(func() error {
|
|
instance, err := charger.NewFromConfig(cc.Type, cc.Other)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create charger '%s': %w", cc.Name, err)
|
|
}
|
|
|
|
return config.Chargers().Add(config.NewStaticDevice(cc, instance))
|
|
})
|
|
}
|
|
|
|
// append devices from database
|
|
configurable, err := config.ConfigurationsByClass(templates.Charger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, conf := range configurable {
|
|
g.Go(func() error {
|
|
cc := conf.Named()
|
|
|
|
if len(names) > 0 && !slices.Contains(names, cc.Name) {
|
|
return nil
|
|
}
|
|
|
|
instance, err := charger.NewFromConfig(cc.Type, cc.Other)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create charger '%s': %w", cc.Name, err)
|
|
}
|
|
|
|
return config.Chargers().Add(config.NewConfigurableDevice(conf, instance))
|
|
})
|
|
}
|
|
|
|
return g.Wait()
|
|
}
|
|
|
|
func vehicleInstance(cc config.Named) (api.Vehicle, error) {
|
|
instance, err := vehicle.NewFromConfig(cc.Type, cc.Other)
|
|
if err != nil {
|
|
var ce *util.ConfigError
|
|
if errors.As(err, &ce) {
|
|
return nil, err
|
|
}
|
|
|
|
// wrap non-config vehicle errors to prevent fatals
|
|
log.ERROR.Printf("creating vehicle %s failed: %v", cc.Name, err)
|
|
instance = vehicle.NewWrapper(cc.Name, cc.Type, cc.Other, err)
|
|
}
|
|
|
|
// ensure vehicle config has title
|
|
if instance.Title() == "" {
|
|
//lint:ignore SA1019 as Title is safe on ascii
|
|
instance.SetTitle(strings.Title(cc.Name))
|
|
}
|
|
|
|
return instance, nil
|
|
}
|
|
|
|
func configureVehicles(static []config.Named, names ...string) error {
|
|
var mu sync.Mutex
|
|
g, _ := errgroup.WithContext(context.Background())
|
|
|
|
// stable-sort vehicles by name
|
|
devs1 := make([]config.Device[api.Vehicle], 0, len(static))
|
|
|
|
for i, cc := range static {
|
|
if cc.Name == "" {
|
|
return fmt.Errorf("cannot create vehicle %d: missing name", i+1)
|
|
}
|
|
|
|
if len(names) > 0 && !slices.Contains(names, cc.Name) {
|
|
continue
|
|
}
|
|
|
|
if err := nameValid(cc.Name); err != nil {
|
|
return fmt.Errorf("cannot create vehicle %d: %w", i+1, err)
|
|
}
|
|
|
|
g.Go(func() error {
|
|
instance, err := vehicleInstance(cc)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create vehicle '%s': %w", cc.Name, err)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
devs1 = append(devs1, config.NewStaticDevice(cc, instance))
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// append devices from database
|
|
configurable, err := config.ConfigurationsByClass(templates.Vehicle)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// stable-sort vehicles by id
|
|
devs2 := make([]config.ConfigurableDevice[api.Vehicle], 0, len(configurable))
|
|
|
|
for _, conf := range configurable {
|
|
g.Go(func() error {
|
|
cc := conf.Named()
|
|
|
|
if len(names) > 0 && !slices.Contains(names, cc.Name) {
|
|
return nil
|
|
}
|
|
|
|
instance, err := vehicleInstance(cc)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create vehicle '%s': %w", cc.Name, err)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
devs2 = append(devs2, config.NewConfigurableDevice(conf, instance))
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if err := g.Wait(); err != nil {
|
|
return err
|
|
}
|
|
|
|
slices.SortFunc(devs1, func(i, j config.Device[api.Vehicle]) int {
|
|
return cmp.Compare(strings.ToLower(i.Config().Name), strings.ToLower(j.Config().Name))
|
|
})
|
|
|
|
for _, dev := range devs1 {
|
|
if err := config.Vehicles().Add(dev); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
slices.SortFunc(devs2, func(i, j config.ConfigurableDevice[api.Vehicle]) int {
|
|
return cmp.Compare(i.ID(), j.ID())
|
|
})
|
|
|
|
for _, dev := range devs2 {
|
|
if err := config.Vehicles().Add(dev); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func configureEnvironment(cmd *cobra.Command, conf globalConfig) (err error) {
|
|
// full http request log
|
|
if cmd.Flags().Lookup(flagHeaders).Changed {
|
|
request.LogHeaders = true
|
|
}
|
|
|
|
// setup machine id
|
|
if conf.Plant != "" {
|
|
err = machine.CustomID(conf.Plant)
|
|
}
|
|
|
|
// setup sponsorship (allow env override)
|
|
if err == nil {
|
|
err = sponsor.ConfigureSponsorship(conf.SponsorToken)
|
|
}
|
|
|
|
// setup translations
|
|
if err == nil {
|
|
err = locale.Init()
|
|
}
|
|
|
|
// setup persistence
|
|
if err == nil && conf.Database.Dsn != "" {
|
|
err = configureDatabase(conf.Database)
|
|
}
|
|
|
|
// setup mqtt client listener
|
|
if err == nil && conf.Mqtt.Broker != "" {
|
|
err = configureMQTT(conf.Mqtt)
|
|
}
|
|
|
|
// setup javascript VMs
|
|
if err == nil {
|
|
err = configureJavascript(conf.Javascript)
|
|
}
|
|
|
|
// setup go VMs
|
|
if err == nil {
|
|
err = configureGo(conf.Go)
|
|
}
|
|
|
|
// setup EEBus server
|
|
if err == nil && conf.EEBus != nil {
|
|
err = configureEEBus(conf.EEBus)
|
|
}
|
|
|
|
// setup config database
|
|
if err == nil {
|
|
err = config.Init(db.Instance)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// configureDatabase configures session database
|
|
func configureDatabase(conf dbConfig) error {
|
|
if err := db.NewInstance(conf.Type, conf.Dsn); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := settings.Init(); err != nil {
|
|
return err
|
|
}
|
|
|
|
persistSettings := func() {
|
|
if err := settings.Persist(); err != nil {
|
|
log.ERROR.Println("cannot save settings:", err)
|
|
}
|
|
}
|
|
|
|
// persist unsaved settings on shutdown
|
|
shutdown.Register(persistSettings)
|
|
|
|
// persist unsaved settings every 30 minutes
|
|
go func() {
|
|
for range time.Tick(30 * time.Minute) {
|
|
persistSettings()
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// configureInflux configures influx database
|
|
func configureInflux(conf server.InfluxConfig, site site.API, in <-chan util.Param) {
|
|
influx := server.NewInfluxClient(
|
|
conf.URL,
|
|
conf.Token,
|
|
conf.Org,
|
|
conf.User,
|
|
conf.Password,
|
|
conf.Database,
|
|
)
|
|
|
|
// eliminate duplicate values
|
|
dedupe := pipe.NewDeduplicator(30*time.Minute, "vehicleCapacity", "vehicleSoc", "vehicleRange", "vehicleOdometer", "chargedEnergy", "chargeRemainingEnergy")
|
|
in = dedupe.Pipe(in)
|
|
|
|
go influx.Run(site, in)
|
|
}
|
|
|
|
// setup mqtt
|
|
func configureMQTT(conf mqttConfig) error {
|
|
log := util.NewLogger("mqtt")
|
|
|
|
instance, err := mqtt.RegisteredClient(log, conf.Broker, conf.User, conf.Password, conf.ClientID, 1, conf.Insecure, func(options *paho.ClientOptions) {
|
|
topic := fmt.Sprintf("%s/status", strings.Trim(conf.Topic, "/"))
|
|
options.SetWill(topic, "offline", 1, true)
|
|
|
|
oc := options.OnConnect
|
|
options.SetOnConnectHandler(func(client paho.Client) {
|
|
oc(client) // original handler
|
|
_ = client.Publish(topic, 1, true, "online") // alive - not logged
|
|
})
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed configuring mqtt: %w", err)
|
|
}
|
|
|
|
mqtt.Instance = instance
|
|
return nil
|
|
}
|
|
|
|
// setup javascript
|
|
func configureJavascript(conf []javascriptConfig) error {
|
|
for _, cc := range conf {
|
|
if _, err := javascript.RegisteredVM(cc.VM, cc.Script); err != nil {
|
|
return fmt.Errorf("failed configuring javascript: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setup go
|
|
func configureGo(conf []goConfig) error {
|
|
for _, cc := range conf {
|
|
if _, err := golang.RegisteredVM(cc.VM, cc.Script); err != nil {
|
|
return fmt.Errorf("failed configuring go: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setup HEMS
|
|
func configureHEMS(conf config.Typed, site *core.Site, httpd *server.HTTPd) error {
|
|
hems, err := hems.NewFromConfig(conf.Type, conf.Other, site, httpd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed configuring hems: %w", err)
|
|
}
|
|
|
|
go hems.Run()
|
|
|
|
return nil
|
|
}
|
|
|
|
// setup MDNS
|
|
func configureMDNS(conf networkConfig) error {
|
|
host := strings.TrimSuffix(conf.Host, ".local")
|
|
|
|
zc, err := zeroconf.RegisterProxy("evcc", "_http._tcp", "local.", conf.Port, host, nil, []string{"path=/"}, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("mDNS announcement: %w", err)
|
|
}
|
|
|
|
shutdown.Register(zc.Shutdown)
|
|
|
|
return nil
|
|
}
|
|
|
|
// setup EEBus
|
|
func configureEEBus(conf map[string]interface{}) error {
|
|
var err error
|
|
if eebus.Instance, err = eebus.NewServer(conf); err != nil {
|
|
return fmt.Errorf("failed configuring eebus: %w", err)
|
|
}
|
|
|
|
eebus.Instance.Run()
|
|
shutdown.Register(eebus.Instance.Shutdown)
|
|
|
|
return nil
|
|
}
|
|
|
|
// setup messaging
|
|
func configureMessengers(conf messagingConfig, vehicles push.Vehicles, valueChan chan util.Param, cache *util.Cache) (chan push.Event, error) {
|
|
messageChan := make(chan push.Event, 1)
|
|
|
|
messageHub, err := push.NewHub(conf.Events, vehicles, cache)
|
|
if err != nil {
|
|
return messageChan, fmt.Errorf("failed configuring push services: %w", err)
|
|
}
|
|
|
|
for _, service := range conf.Services {
|
|
impl, err := push.NewFromConfig(service.Type, service.Other)
|
|
if err != nil {
|
|
return messageChan, fmt.Errorf("failed configuring push service %s: %w", service.Type, err)
|
|
}
|
|
messageHub.Add(impl)
|
|
}
|
|
|
|
go messageHub.Run(messageChan, valueChan)
|
|
|
|
return messageChan, nil
|
|
}
|
|
|
|
func configureTariff(name string, conf config.Typed, t *api.Tariff, wg *sync.WaitGroup) {
|
|
defer wg.Done()
|
|
|
|
if conf.Type == "" {
|
|
return
|
|
}
|
|
|
|
res, err := tariff.NewFromConfig(conf.Type, conf.Other)
|
|
if err != nil {
|
|
log.ERROR.Printf("failed configuring %s tariff: %v", name, err)
|
|
return
|
|
}
|
|
|
|
*t = res
|
|
}
|
|
|
|
func configureTariffs(conf tariffConfig) (*tariff.Tariffs, error) {
|
|
tariffs := tariff.Tariffs{
|
|
Currency: currency.EUR,
|
|
}
|
|
|
|
if conf.Currency != "" {
|
|
tariffs.Currency = currency.MustParseISO(conf.Currency)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(4)
|
|
|
|
go configureTariff("grid", conf.Grid, &tariffs.Grid, &wg)
|
|
go configureTariff("feedin", conf.FeedIn, &tariffs.FeedIn, &wg)
|
|
go configureTariff("co2", conf.Co2, &tariffs.Co2, &wg)
|
|
go configureTariff("planner", conf.Planner, &tariffs.Planner, &wg)
|
|
|
|
wg.Wait()
|
|
|
|
return &tariffs, nil
|
|
}
|
|
|
|
func configureDevices(conf globalConfig) error {
|
|
if err := configureMeters(conf.Meters); err != nil {
|
|
return err
|
|
}
|
|
if err := configureChargers(conf.Chargers); err != nil {
|
|
return err
|
|
}
|
|
if err := configureCircuits(conf.Circuits); err != nil {
|
|
return err
|
|
}
|
|
return configureVehicles(conf.Vehicles)
|
|
}
|
|
|
|
func configureSiteAndLoadpoints(conf globalConfig) (*core.Site, error) {
|
|
if err := configureDevices(conf); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
loadpoints, err := configureLoadpoints(conf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed configuring loadpoints: %w", err)
|
|
}
|
|
|
|
tariffs, err := configureTariffs(conf.Tariffs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
site, err := configureSite(conf.Site, loadpoints, tariffs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(config.Circuits().Devices()) > 0 {
|
|
if err := validateCircuits(site, loadpoints); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return site, nil
|
|
}
|
|
|
|
func validateCircuits(site site.API, loadpoints []*core.Loadpoint) error {
|
|
CONTINUE:
|
|
for _, dev := range config.Circuits().Devices() {
|
|
instance := dev.Instance()
|
|
|
|
if instance.HasMeter() || site.GetCircuit() == instance {
|
|
continue
|
|
}
|
|
|
|
for _, lp := range loadpoints {
|
|
if lp.GetCircuit() == instance {
|
|
continue CONTINUE
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("circuit %s has no meter or loadpoint assigned", dev.Config().Name)
|
|
}
|
|
|
|
if site.GetCircuit() == nil {
|
|
return errors.New("site has no circuit")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, tariffs *tariff.Tariffs) (*core.Site, error) {
|
|
site, err := core.NewSiteFromConfig(log, conf, loadpoints, tariffs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed configuring site: %w", err)
|
|
}
|
|
|
|
if len(config.Circuits().Devices()) > 0 && site.GetCircuit() == nil {
|
|
return nil, errors.New("site has no circuit")
|
|
}
|
|
|
|
return site, nil
|
|
}
|
|
|
|
func configureLoadpoints(conf globalConfig) ([]*core.Loadpoint, error) {
|
|
if len(conf.Loadpoints) == 0 {
|
|
return nil, errors.New("missing loadpoints")
|
|
}
|
|
|
|
var loadpoints []*core.Loadpoint
|
|
|
|
for id, cfg := range conf.Loadpoints {
|
|
log := util.NewLoggerWithLoadpoint("lp-"+strconv.Itoa(id+1), id+1)
|
|
settings := &core.Settings{Key: "lp" + strconv.Itoa(id+1) + "."}
|
|
|
|
lp, err := core.NewLoadpointFromConfig(log, settings, cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed configuring loadpoint: %w", err)
|
|
}
|
|
|
|
loadpoints = append(loadpoints, lp)
|
|
}
|
|
|
|
return loadpoints, nil
|
|
}
|
|
|
|
// configureAuth handles routing for devices. For now only api.AuthProvider related routes
|
|
func configureAuth(conf networkConfig, vehicles []api.Vehicle, 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
|
|
oauth2redirect.SetupRouter(auth)
|
|
|
|
// initialize
|
|
authCollection := util.NewAuthCollection(paramC)
|
|
|
|
baseURI := conf.URI()
|
|
baseAuthURI := fmt.Sprintf("%s/oauth", baseURI)
|
|
|
|
var id int
|
|
for _, v := range 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 := authCollection.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)
|
|
}
|
|
}
|
|
|
|
authCollection.Publish()
|
|
}
|