package cmd import ( "cmp" "context" "errors" "fmt" "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/api/globalconfig" "github.com/evcc-io/evcc/charger" "github.com/evcc-io/evcc/charger/ocpp" "github.com/evcc-io/evcc/cmd/shutdown" "github.com/evcc-io/evcc/core" "github.com/evcc-io/evcc/core/circuit" "github.com/evcc-io/evcc/core/keys" "github.com/evcc-io/evcc/core/loadpoint" coresettings "github.com/evcc-io/evcc/core/settings" "github.com/evcc-io/evcc/hems" hemsapi "github.com/evcc-io/evcc/hems/hems" "github.com/evcc-io/evcc/hems/shm" "github.com/evcc-io/evcc/messenger" "github.com/evcc-io/evcc/meter" "github.com/evcc-io/evcc/plugin/golang" "github.com/evcc-io/evcc/plugin/javascript" "github.com/evcc-io/evcc/plugin/mqtt" "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/eebus" "github.com/evcc-io/evcc/server/modbus" "github.com/evcc-io/evcc/server/providerauth" "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/request" _ "github.com/evcc-io/evcc/util/service" "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/samber/lo" "github.com/spf13/cast" "github.com/spf13/cobra" vpr "github.com/spf13/viper" "golang.org/x/sync/errgroup" "golang.org/x/text/currency" ) var conf = globalconfig.All{ Interval: 30 * time.Second, Log: "info", Network: globalconfig.Network{ Host: "", Port: 7070, }, Ocpp: ocpp.Config{ Port: 8887, }, Mqtt: globalconfig.Mqtt{ Topic: "evcc", }, EEBus: eebus.Config{ Port: 4712, }, Database: globalconfig.DB{ Type: "sqlite", Dsn: "", }, } var yamlSource struct { sponsor globalconfig.YamlSource hems globalconfig.YamlSource eebus globalconfig.YamlSource tariffs globalconfig.YamlSource messaging globalconfig.YamlSource } var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`) 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 loadConfigFile(conf *globalconfig.All, checkDB bool) error { if err := viper.ReadInConfig(); err != nil { if _, ok := errors.AsType[vpr.ConfigFileNotFoundError](err); !ok { return fmt.Errorf("failed reading config file: %w", err) } } if cfgFile := viper.ConfigFileUsed(); cfgFile != "" { log.INFO.Println("using config file:", cfgFile) } else { log.INFO.Println("config file not found, database-only mode") } if err := viper.UnmarshalExact(conf); err != nil { return fmt.Errorf("failed parsing config file: %w", err) } // user did not specify a database path if conf.Database.Dsn == "" && checkDB { // check if service database exists if _, err := os.Stat(serviceDB); err == nil { // service database found, ask user what to do sudo := "" if !isWritable(serviceDB) { sudo = "sudo " } log.FATAL.Fatal(` Found systemd service database at "` + serviceDB + `", evcc has been invoked with no explicit database path. Running the same config with multiple databases can lead to expiring vehicle tokens. If you want to use the existing service database run the following command: ` + sudo + `evcc --database ` + serviceDB + ` If you want to create a new user-space database run the following command: evcc --database ~/.evcc/evcc.db If you know what you're doing, you can skip the database check with the --ignore-db flag. `) } } // parse log levels after reading config parseLogLevels() return nil } func isWritable(filePath string) bool { file, err := os.OpenFile(filePath, os.O_WRONLY, 0o666) if err != nil { return false } file.Close() return true } func configureCircuits(conf *[]config.Named) error { // migrate settings if settings.Exists(keys.Circuits) { *conf = []config.Named{} if err := settings.Yaml(keys.Circuits, new([]map[string]any), &conf); err != nil { return err } } children := slices.Clone(*conf) // 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) props, err := customDevice(cc.Other) if err != nil { return fmt.Errorf("cannot decode custom circuit '%s': %w", cc.Name, err) } instance, err := circuit.NewFromConfig(context.TODO(), log, props) 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) } 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 } type newFromConfFunc[T any] func(context.Context, string, map[string]any) (T, error) func staticInstance[T any](typ string, cc config.Named, newFromConf newFromConfFunc[T], h config.Handler[T]) error { ctx := util.WithLogger(context.TODO(), util.NewLogger(cc.Name)) instance, err := newFromConf(ctx, cc.Type, cc.Other) if err != nil { return &DeviceError{cc.Name, fmt.Errorf("cannot create %s '%s': %w", typ, cc.Name, err)} } if err := h.Add(config.NewStaticDevice(cc, instance)); err != nil { return &DeviceError{cc.Name, err} } return nil } func configurableInstance[T any](typ string, conf *config.Config, newFromConf newFromConfFunc[T], h config.Handler[T]) error { cc := conf.Named() ctx := util.WithLogger(context.TODO(), util.NewLogger(cc.Name)) props, err := customDevice(cc.Other) if err != nil { err = &DeviceError{cc.Name, fmt.Errorf("cannot decode custom %s '%s': %w", typ, cc.Name, err)} } var instance T if err == nil { instance, err = newFromConf(ctx, cc.Type, props) if err != nil { err = &DeviceError{cc.Name, fmt.Errorf("cannot create %s '%s': %w", typ, cc.Name, err)} } } if e := h.Add(config.NewConfigurableDevice(conf, instance)); e != nil && err == nil { err = &DeviceError{cc.Name, e} } return err } func configureMeters(static []config.Named, names ...string) error { var eg errgroup.Group for i, cc := range static { if cc.Name == "" { return fmt.Errorf("cannot create meter %d: missing name", i+1) } // configure all, if no name refs are given 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) } eg.Go(func() error { return staticInstance("meter", cc, meter.NewFromConfig, config.Meters()) }) } // append devices from database configurable, err := config.ConfigurationsByClass(templates.Meter) if err != nil { return err } for _, conf := range configurable { eg.Go(func() error { cc := conf.Named() // always skip unreferenced db devices if !slices.Contains(names, cc.Name) { return nil } return configurableInstance("meter", &conf, meter.NewFromConfig, config.Meters()) }) } return eg.Wait() } func configureChargers(static []config.Named, names ...string) error { var eg errgroup.Group for i, cc := range static { if cc.Name == "" { return fmt.Errorf("cannot create charger %d: missing name", i+1) } // configure all, if no name refs are given 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) } eg.Go(func() error { return staticInstance("charger", cc, charger.NewFromConfig, config.Chargers()) }) } // append devices from database configurable, err := config.ConfigurationsByClass(templates.Charger) if err != nil { return err } for _, conf := range configurable { eg.Go(func() error { cc := conf.Named() // always skip unreferenced db devices if !slices.Contains(names, cc.Name) { return nil } return configurableInstance("charger", &conf, charger.NewFromConfig, config.Chargers()) }) } return eg.Wait() } func vehicleInstance(cc config.Named) (api.Vehicle, error) { ctx := util.WithLogger(context.TODO(), util.NewLogger(cc.Name)) props, err := customDevice(cc.Other) var instance api.Vehicle if err == nil { instance, err = vehicle.NewFromConfig(ctx, cc.Type, props) } if err != nil { if _, ok := errors.AsType[*util.ConfigError](err); ok { 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.GetTitle() == "" { //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 var eg errgroup.Group // 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) } eg.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 { eg.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 := eg.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 configureSponsorship(token string) (err error) { if settings.Exists(keys.SponsorToken) { if token, err = settings.String(keys.SponsorToken); err != nil { return err } } else if token != "" { yamlSource.sponsor = globalconfig.YamlSourceFile } return sponsor.ConfigureSponsorship(token) } func configureEnvironment(cmd *cobra.Command, conf *globalconfig.All) error { // full http request log if cmd.Flag(flagHeaders).Changed { request.LogHeaders = true } // setup persistence err := wrapErrorWithClass(ClassDatabase, configureDatabase(conf.Database)) // setup additional templates if err == nil { if cmd.Flags().Changed(flagTemplate) { class, err := templates.ClassString(cmd.Flags().Lookup(flagTemplateType).Value.String()) if err != nil { return err } if err := templates.Register(class, cmd.Flag(flagTemplate).Value.String()); err != nil { return err } } } // setup translations if err == nil { // TODO decide wrapping err = locale.Init() } // setup machine id if err == nil && conf.Plant != "" { // TODO decide wrapping err = machine.CustomID(conf.Plant) } // setup sponsorship (allow env override) if err == nil { err = wrapErrorWithClass(ClassSponsorship, configureSponsorship(conf.SponsorToken)) } // setup mqtt client listener if err == nil { err = wrapErrorWithClass(ClassMqtt, configureMqtt(&conf.Mqtt)) } // setup OCPP server if err == nil { configureOCPP(&conf.Ocpp, conf.Network.ExternalUrl) } // setup EEBus server if err == nil { err = wrapErrorWithClass(ClassEEBus, configureEEBus(&conf.EEBus)) } // setup javascript VMs if err == nil { err = wrapErrorWithClass(ClassJavascript, configureJavascript(conf.Javascript)) } // setup go VMs if err == nil { err = wrapErrorWithClass(ClassGo, configureGo(conf.Go)) } return err } // configureDatabase configures session database func configureDatabase(conf globalconfig.DB) error { if conf.Dsn == "" { conf.Dsn = userDB } if err := db.NewInstance(conf.Type, conf.Dsn); 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(time.Minute) { persistSettings() } }() return nil } // configureInflux configures influx database func configureInflux(conf *globalconfig.Influx) (*server.Influx, error) { // read settings if settings.Exists(keys.Influx) { if err := settings.Json(keys.Influx, &conf); err != nil { return nil, err } } if conf.URL == "" { return nil, nil } influx := server.NewInfluxClient( conf.URL, conf.Token, conf.Org, conf.User, conf.Password, conf.Database, conf.Insecure, ) return influx, nil } // setup mqtt func configureMqtt(conf *globalconfig.Mqtt) error { // migrate settings if settings.Exists(keys.Mqtt) { if err := settings.Json(keys.Mqtt, &conf); err != nil { return err } } if conf.Broker == "" { return nil } log := util.NewLogger("mqtt") instance, err := mqtt.RegisteredClient(log, conf.Broker, conf.User, conf.Password, conf.ClientID, 1, conf.Insecure, conf.CaCert, conf.ClientCert, conf.ClientKey, func(options *paho.ClientOptions) { if !runAsService || conf.Topic == "" { return } 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 SHM func configureSHM(conf *shm.Config, externalUrl string, site *core.Site, httpd *server.HTTPd) error { if settings.Exists(keys.Shm) { if err := settings.Json(keys.Shm, &conf); err != nil { return err } } if err := shm.NewFromConfig(*conf, externalUrl, site, httpd.Addr, httpd.Router()); err != nil { return fmt.Errorf("failed configuring shm: %w", err) } return nil } // setup javascript func configureJavascript(conf []globalconfig.Javascript) 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 []globalconfig.Go) 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 *globalconfig.Hems, site *core.Site) (hemsapi.API, error) { // use yaml if configured if conf.Type == "" { if settings.Exists(keys.Hems) { *conf = globalconfig.Hems{} if err := settings.Yaml(keys.Hems, new(map[string]any), &conf); err != nil { return nil, err } yamlSource.hems = globalconfig.YamlSourceDb } } else { yamlSource.hems = globalconfig.YamlSourceFile } if conf.Type == "" { return nil, nil } props, err := customDevice(conf.Other) if err != nil { return nil, fmt.Errorf("cannot decode custom hems '%s': %w", conf.Type, err) } hems, err := hems.NewFromConfig(context.TODO(), conf.Type, props, site) if err != nil { return nil, fmt.Errorf("failed configuring hems: %w", err) } go hems.Run() return hems, nil } // networkSettings reads/migrates network settings func networkSettings(conf *globalconfig.Network) error { if settings.Exists(keys.Network) { return settings.Json(keys.Network, &conf) } return nil } // setup MDNS func configureMDNS(conf globalconfig.Network) error { host := strings.TrimSuffix(conf.Host, ".local") if host == "" { host = "evcc" } internalURL := conf.InternalURL() text := []string{"path=/", "internal_url=" + internalURL} if externalURL := conf.ExternalURL(); externalURL != internalURL { text = append(text, "external_url="+externalURL) } zc, err := zeroconf.RegisterProxy("evcc", "_http._tcp", "local.", conf.Port, host, nil, text, nil) if err != nil { return err } shutdown.Register(zc.Shutdown) return nil } // setup OCPP func configureOCPP(cfg *ocpp.Config, externalUrl string) { ocpp.Init(*cfg, externalUrl) } // setup EEBus func configureEEBus(conf *eebus.Config) error { // migrate settings if settings.Exists(keys.EEBus) { if err := migrateYamlToJson(keys.EEBus, conf); err != nil { return err } } else if conf.IsConfigured() { yamlSource.eebus = globalconfig.YamlSourceFile } if !conf.IsConfigured() { cc, err := eebus.DefaultConfig(conf) if err != nil { return err } *conf = *cc if err := settings.SetJson(keys.EEBus, conf); err != nil { return err } } 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 } func configureMessengers(confMessaging *globalconfig.Messaging, confEvents *globalconfig.MessagingEvents, vehicles messenger.Vehicles, valueChan chan<- util.Param, cache *util.ParamCache) (chan messenger.Event, error) { // yaml config from file if len(confMessaging.Events) != 0 || len(confMessaging.Services) != 0 { yamlSource.messaging = globalconfig.YamlSourceFile } // yaml config from db (deprecated) if settings.Exists(keys.Messaging) { if yamlSource.messaging == globalconfig.YamlSourceFile { // just warn, no error to not break previous behavior log.WARN.Println("messaging configured via UI yaml; evcc.yaml config will be ignored") } *confMessaging = globalconfig.Messaging{} if err := settings.Yaml(keys.Messaging, new(map[string]any), &confMessaging); err != nil { return nil, err } yamlSource.messaging = globalconfig.YamlSourceDb } if settings.Exists(keys.MessagingEvents) { *confEvents = globalconfig.MessagingEvents{} if err := settings.Json(keys.MessagingEvents, &confEvents); err != nil { return nil, err } if yamlSource.messaging != globalconfig.YamlSourceNone && confEvents != nil { return nil, errors.New("yaml and device config exists for messaging; remove yaml config") } } messageChan := make(chan messenger.Event, 1) var eg errgroup.Group for i, cc := range confMessaging.Services { // add name for meter/charger parity cc := config.Named{ Name: fmt.Sprintf("push-%d", i+1), Type: cc.Type, Other: cc.Other, } eg.Go(func() error { return staticInstance("messenger", cc, messenger.NewFromConfig, config.Messengers()) }) } // append devices from database configurable, err := config.ConfigurationsByClass(templates.Messenger) if err != nil { return messageChan, err } for _, conf := range configurable { eg.Go(func() error { return configurableInstance("messenger", &conf, messenger.NewFromConfig, config.Messengers()) }) } if err := eg.Wait(); err != nil { return messageChan, &ClassError{ClassMessenger, err} } var events globalconfig.MessagingEvents if len(*confEvents) > 0 { events = conf.MessagingEvents } else { events = confMessaging.Events } messageHub, err := messenger.NewHub(events, vehicles, cache) if err != nil { return messageChan, fmt.Errorf("failed configuring push services: %w", err) } for _, dev := range config.Messengers().Devices() { if inst := dev.Instance(); inst != nil { messageHub.Add(inst) } } go messageHub.Run(messageChan, valueChan) return messageChan, nil } func tariffInstance(name string, conf config.Typed) (api.Tariff, error) { ctx := util.WithLogger(context.TODO(), util.NewLogger(name)) props, err := customDevice(conf.Other) if err != nil { return nil, fmt.Errorf("cannot decode custom tariff '%s': %w", name, err) } instance, err := tariff.NewFromConfig(ctx, conf.Type, props) if err != nil { if _, ok := errors.AsType[*util.ConfigError](err); ok { return nil, err } // wrap non-config tariff errors to prevent fatals log.ERROR.Printf("creating tariff %s failed: %v", name, err) instance = tariff.NewWrapper(conf.Type, conf.Other, err) } return instance, nil } func configureSolarTariff(conf []config.Typed, t *api.Tariff) error { var eg errgroup.Group tt := make([]api.Tariff, len(conf)) for i, conf := range conf { eg.Go(func() error { if conf.Type == "" { return errors.New("missing type") } name := fmt.Sprintf("%s-%s-%d", api.TariffUsageSolar, tariff.Name(conf), i) res, err := tariffInstance(name, conf) if err != nil { return &DeviceError{name, err} } tt[i] = res return nil }) } if err := eg.Wait(); err != nil { return err } *t = tariff.NewCombined(tt) return nil } func configureTariff(conf config.Typed, deviceName string, target *api.Tariff) error { if conf.Type != "" { instance, err := tariffInstance("tariff", conf) if err != nil { return err } *target = instance return nil } if deviceName != "" { dev, err := config.Tariffs().ByName(deviceName) if err != nil { return fmt.Errorf("tariff device not found: %w", err) } *target = dev.Instance() } return nil } func configureSolarTariffs(confs []config.Typed, deviceNames []string, target *api.Tariff) error { if len(confs) > 0 { if len(confs) == 1 { return configureTariff(confs[0], "", target) } return configureSolarTariff(confs, target) } if len(deviceNames) > 0 { if len(deviceNames) == 1 { return configureTariff(config.Typed{}, deviceNames[0], target) } tt := make([]api.Tariff, len(deviceNames)) for i, name := range deviceNames { dev, err := config.Tariffs().ByName(name) if err != nil { return fmt.Errorf("tariff device %s not found: %w", name, err) } tt[i] = dev.Instance() } *target = tariff.NewCombined(tt) } return nil } func configureTariffs(conf *globalconfig.Tariffs, names ...string) (*tariff.Tariffs, error) { tariffs := tariff.Tariffs{ Currency: currency.EUR, } // yaml config from file if conf.IsConfigured() { yamlSource.tariffs = globalconfig.YamlSourceFile } // yaml config from db (deprecated) if settings.Exists(keys.Tariffs) { if yamlSource.tariffs == globalconfig.YamlSourceFile { // just warn, no error to not break previous behavior log.WARN.Println("tariffs configured via UI; evcc.yaml config will be ignored") } *conf = globalconfig.Tariffs{} if err := settings.Yaml(keys.Tariffs, new(map[string]any), &conf); err != nil { return &tariffs, err } yamlSource.tariffs = globalconfig.YamlSourceDb } // device config from db var refs globalconfig.TariffRefs if settings.Exists(keys.TariffRefs) { if err := settings.Json(keys.TariffRefs, &refs); err != nil { return &tariffs, err } if yamlSource.tariffs != globalconfig.YamlSourceNone && refs.IsConfigured() { return &tariffs, errors.New("tariffs are configured via UI; having an additional yaml config is not allowed") } } // load tariff devices from database configurable, err := config.ConfigurationsByClass(templates.Tariff) if err != nil { return &tariffs, err } var eg errgroup.Group for _, conf := range configurable { eg.Go(func() error { cc := conf.Named() if len(names) > 0 && !slices.Contains(names, cc.Name) { return nil } instance, err := tariffInstance(cc.Name, config.Typed{Type: cc.Type, Other: cc.Other}) if err != nil { return err } if e := config.Tariffs().Add(config.NewConfigurableDevice(&conf, instance)); e != nil { return &DeviceError{cc.Name, e} } return nil }) } if err := eg.Wait(); err != nil { return &tariffs, err } // resolve tariff roles eg = errgroup.Group{} eg.Go(func() error { return configureTariff(conf.Grid, refs.Grid, &tariffs.Grid) }) eg.Go(func() error { return configureTariff(conf.FeedIn, refs.FeedIn, &tariffs.FeedIn) }) eg.Go(func() error { return configureTariff(conf.Co2, refs.Co2, &tariffs.Co2) }) eg.Go(func() error { return configureTariff(conf.Planner, refs.Planner, &tariffs.Planner) }) eg.Go(func() error { return configureSolarTariffs(conf.Solar, refs.Solar, &tariffs.Solar) }) if err := eg.Wait(); err != nil { return &tariffs, &ClassError{ClassTariff, err} } // validate currency if cur, _ := settings.String(keys.Currency); cur != "" { conf.Currency = cur } if conf.Currency != "" { cur, err := currency.ParseISO(conf.Currency) if err != nil { return &tariffs, err } tariffs.Currency = cur } return &tariffs, nil } func configureDevices(conf globalconfig.All) error { // collect references for filtering used devices if err := collectRefs(conf); err != nil { return err } // make sure all devices are configured var errs []error if err := configureMeters(conf.Meters, references.meter...); err != nil { errs = append(errs, &ClassError{ClassMeter, err}) } if err := configureChargers(conf.Chargers, references.charger...); err != nil { errs = append(errs, &ClassError{ClassCharger, err}) } if err := configureVehicles(conf.Vehicles); err != nil { errs = append(errs, &ClassError{ClassVehicle, err}) } if err := configureCircuits(&conf.Circuits); err != nil { errs = append(errs, &ClassError{ClassCircuit, err}) } return joinErrors(errs...) } func configureModbusProxy(conf *[]globalconfig.ModbusProxy) error { if settings.Exists(keys.ModbusProxy) { if err := migrateYamlToJson(keys.ModbusProxy, conf); err != nil { return err } } // prevent panic if conf == nil { return nil } for _, cfg := range *conf { var mode modbus.ReadOnlyMode mode, err := modbus.ReadOnlyModeString(cfg.ReadOnly) if err != nil { return err } if err = modbus.StartProxy(cfg.Port, cfg.Settings, mode); err != nil { return err } } return nil } func configureSiteAndLoadpoints(conf *globalconfig.All) (*core.Site, error) { // migrate settings if settings.Exists(keys.Interval) { d, err := settings.Int(keys.Interval) if err != nil { return nil, err } conf.Interval = time.Duration(d) } var errs []error if err := configureDevices(*conf); err != nil { errs = append(errs, err) } if err := configureLoadpoints(*conf); err != nil { errs = append(errs, &ClassError{ClassLoadpoint, err}) } tariffs, err := configureTariffs(&conf.Tariffs, references.tariff...) if err != nil { errs = append(errs, &ClassError{ClassTariff, err}) } loadpoints := lo.Map(config.Loadpoints().Devices(), func(dev config.Device[loadpoint.API], _ int) *core.Loadpoint { return dev.Instance().(*core.Loadpoint) }) site, err := configureSite(conf.Site, loadpoints, tariffs) if err != nil { errs = append(errs, err) } if len(errs) > 0 { return site, joinErrors(errs...) } if len(config.Circuits().Devices()) > 0 { if err := validateCircuits(loadpoints); err != nil { return site, &ClassError{ClassCircuit, err} } } return site, nil } func validateCircuits(loadpoints []*core.Loadpoint) error { var hasRoot bool CONTINUE: for _, dev := range config.Circuits().Devices() { instance := dev.Instance() isRoot := instance.GetParent() == nil if isRoot { if hasRoot { return errors.New("multiple root circuits") } hasRoot = true } if slices.ContainsFunc(loadpoints, func(lp *core.Loadpoint) bool { return lp.GetCircuit() == instance }) { continue CONTINUE } if !isRoot && !instance.HasMeter() { log.INFO.Printf("circuit %s has no meter and no loadpoint assigned", dev.Config().Name) } } if !hasRoot { return errors.New("missing root circuit") } return nil } func configureSite(conf map[string]any, loadpoints []*core.Loadpoint, tariffs *tariff.Tariffs) (*core.Site, error) { site, err := core.NewSiteFromConfig(conf) if err != nil { return site, err } if err := site.Boot(log, loadpoints, tariffs); err != nil { return site, fmt.Errorf("failed booting site: %w", err) } return site, nil } func configureLoadpoints(conf globalconfig.All) error { for id, cc := range conf.Loadpoints { cc.Name = "lp-" + strconv.Itoa(id+1) log := util.NewLoggerWithLoadpoint(cc.Name, id+1) settings := coresettings.NewDatabaseSettingsAdapter(fmt.Sprintf("lp%d.", id+1)) instance, err := core.NewLoadpointFromConfig(log, settings, cc.Other) if err != nil { return &DeviceError{cc.Name, err} } if err := config.Loadpoints().Add(config.NewStaticDevice(cc, loadpoint.API(instance))); err != nil { return &DeviceError{cc.Name, err} } } // append devices from database configurable, err := config.ConfigurationsByClass(templates.Loadpoint) if err != nil { return err } for _, conf := range configurable { cc := conf.Named() id := len(config.Loadpoints().Devices()) name := "lp-" + strconv.Itoa(id+1) log := util.NewLoggerWithLoadpoint(name, id+1) settings := coresettings.NewConfigSettingsAdapter(log, &conf) dynamic, static, err := loadpoint.SplitConfig(cc.Other) if err != nil { return &DeviceError{cc.Name, err} } instance, err := core.NewLoadpointFromConfig(log, settings, static) if err != nil { err = &DeviceError{cc.Name, err} } dev := config.NewConfigurableDevice[loadpoint.API](&conf, instance) if e := config.Loadpoints().Add(dev); e != nil && err == nil { err = &DeviceError{cc.Name, e} } if instance != nil { // ignore dynamic config in case of startup errors that will leave instance empty if e := dynamic.Apply(instance); e != nil && err == nil { err = &DeviceError{cc.Name, e} } } if err != nil { return err } } return nil } // configureAuth handles routing for devices. For now only api.AuthProvider related routes func configureAuth(router *mux.Router, paramC chan<- util.Param) { auth := router.PathPrefix("/providerauth").Subrouter() auth.Use(handlers.CompressHandler) auth.Use(handlers.CORS( handlers.AllowedHeaders([]string{"Content-Type"}), )) // backwards-compatible revert of https://github.com/evcc-io/evcc/pull/21266 router.PathPrefix("/oauth").Handler(auth) // wire the handler providerauth.Setup(auth, paramC) } // isExperimental returns if experimental features are enabled func isExperimental() bool { b, _ := settings.Bool(keys.Experimental) return b }