Skip to content

Commit

Permalink
Experimentelle geführte Erstellung der Konfigurationsdatei (evcc-io#1888
Browse files Browse the repository at this point in the history
)

Dies ist eine experimentelle Umsetzung im folgenden aufgeführten Ziele.

Wie es verwendet:
- Start über `evcc configure`
- Die Konfiguration wird in evcc.yaml geschrieben. Falls diese existiert kann ein alternativer Dateiname angegeben werden

Was es kann:
- Eine geführte Erstellung der Konfigurationsdatei
- Direktes Testen ob die Konfiguration jedes Gerätes auch funktioniert
- Konfigurationsabhängigkeiten durch direkte Konfiguration der Abhängigkeit lösen (z.B. Sponsorshipt required)
- Konfiguration in für Anwender bekannte Informationen und möglichst wenig Informationseingabe zu ermöglichen, z.B. anhand von Produkten anstatt aus der Implementierungssicht
- Bisherige Konfiguration funktioniert weiter
  • Loading branch information
DerAndereAndi authored Nov 30, 2021
1 parent 3c311b6 commit 7e41a97
Show file tree
Hide file tree
Showing 37 changed files with 2,951 additions and 71 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ __debug_bin
*.log
*.json
*.yaml
!templates/**/*.yaml
!package*.json
!evcc.dist.yaml
evcc
Expand Down
40 changes: 20 additions & 20 deletions charger/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,27 @@ import (
"github.com/evcc-io/evcc/util/test"
)

func TestChargers(t *testing.T) {
test.SkipCI(t)
var acceptable = []string{
"invalid plugin type: ...",
"missing mqtt broker configuration",
"mqtt not configured",
"invalid charger type: nrgkick-bluetooth",
"NRGKick bluetooth is only supported on linux",
"invalid pin:",
"hciconfig provided no response",
"connect: no route to host",
"connect: connection refused",
"error connecting: Network Error",
"i/o timeout",
"recv timeout",
"(Client.Timeout exceeded while awaiting headers)",
"can only have either uri or device", // modbus
"sponsorship required, see https://github.com/evcc-io/evcc#sponsorship",
"eebus not configured",
}

acceptable := []string{
"invalid plugin type: ...",
"missing mqtt broker configuration",
"mqtt not configured",
"invalid charger type: nrgkick-bluetooth",
"NRGKick bluetooth is only supported on linux",
"invalid pin:",
"hciconfig provided no response",
"connect: no route to host",
"connect: connection refused",
"error connecting: Network Error",
"i/o timeout",
"recv timeout",
"(Client.Timeout exceeded while awaiting headers)",
"can only have either uri or device", // modbus
"sponsorship required, see https://github.com/evcc-io/evcc#sponsorship",
"eebus not configured",
}
func TestConfigChargers(t *testing.T) {
test.SkipCI(t)

for _, tmpl := range test.ConfigTemplates("charger") {
tmpl := tmpl
Expand Down
44 changes: 44 additions & 0 deletions charger/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package charger

import (
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/templates"
"gopkg.in/yaml.v3"
)

func init() {
registry.Add("template", NewChargerFromTemplateConfig)
}

func NewChargerFromTemplateConfig(other map[string]interface{}) (api.Charger, error) {
cc := struct {
Template string
Other map[string]interface{} `mapstructure:",remain"`
}{}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

tmpl, err := templates.ByTemplate(cc.Template, templates.Charger)
if err != nil {
return nil, err
}

b, _, err := tmpl.RenderResult(false, other)
if err != nil {
return nil, err
}

var instance struct {
Type string
Other map[string]interface{} `yaml:",inline"`
}

if err := yaml.Unmarshal(b, &instance); err != nil {
return nil, err
}

return NewFromConfig(instance.Type, instance.Other)
}
49 changes: 49 additions & 0 deletions charger/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package charger

import (
"testing"

"github.com/evcc-io/evcc/util/templates"
"github.com/evcc-io/evcc/util/test"
"github.com/thoas/go-funk"
)

func TestChargerTemplates(t *testing.T) {
test.SkipCI(t)

for _, tmpl := range templates.ByClass(templates.Charger) {
tmpl := tmpl

// set default values for all params
values := tmpl.Defaults(true)

// set the template value which is needed for rendering
values["template"] = tmpl.Template

// set modbus default test values
if values[templates.ParamModbus] != nil {
modbusChoices := tmpl.ModbusChoices()
if funk.ContainsString(modbusChoices, templates.ModbusChoiceTCPIP) {
values[templates.ModbusTCPIP] = true
} else {
values[templates.ModbusRS485TCPIP] = true
}
}

t.Run(tmpl.Template, func(t *testing.T) {
t.Parallel()

b, values, err := tmpl.RenderResult(true, values)
if err != nil {
t.Logf("%s: %s", tmpl.Template, b)
t.Error(err)
}

_, err = NewFromConfig("template", values)
if err != nil && !test.Acceptable(err, acceptable) {
t.Logf("%s", tmpl.Template)
t.Error(err)
}
})
}
}
41 changes: 41 additions & 0 deletions cmd/configure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cmd

import (
_ "embed"

"github.com/evcc-io/evcc/cmd/configure"
"github.com/evcc-io/evcc/util"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// configureCmd represents the configure command
var configureCmd = &cobra.Command{
Use: "configure",
Short: "Create an EVCC configuration",
Run: runConfigure,
}

func init() {
rootCmd.AddCommand(configureCmd)
configureCmd.Flags().String("lang", "", "Define the localization to be used (en, de)")
configureCmd.Flags().Bool("expand", false, "Enables rendering expanded configuration files")
}

func runConfigure(cmd *cobra.Command, args []string) {
impl := &configure.CmdConfigure{}

lang, err := cmd.Flags().GetString("lang")
if err != nil {
log.FATAL.Fatal(err)
}

expand, err := cmd.Flags().GetBool("expand")
if err != nil {
panic(err)
}

util.LogLevel(viper.GetString("log"), nil)

impl.Run(log, lang, expand)
}
137 changes: 137 additions & 0 deletions cmd/configure/configure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package configure

import (
"bytes"
_ "embed"
"fmt"
"text/template"

"github.com/Masterminds/sprig/v3"
"github.com/thoas/go-funk"
)

type device struct {
Name string
Title string
LogLevel string
Yaml string
ChargerHasMeter bool // only used with chargers to detect if we need to ask for a charge meter
}

type loadpoint struct {
Title string // TODO Perspektivisch können wir was aus core wiederverwenden, für später
Charger string
ChargeMeter string
Vehicles []string
Mode string
MinCurrent int
MaxCurrent int
Phases int
}

type config struct {
Meters []device
Chargers []device
Vehicles []device
Loadpoints []loadpoint
Site struct { // TODO Perspektivisch können wir was aus core wiederverwenden, für später
Title string
Grid string
PVs []string
Batteries []string
}
LogLevels []string
Hems string
EEBUS string
SponsorToken string
}

type Configure struct {
config config
}

// AddLogLevel adds a log level for a specific device name to the configuration
func (c *Configure) AddLogLevel(name string) {
if name == "" || funk.ContainsString(c.config.LogLevels, name) {
return
}
c.config.LogLevels = append(c.config.LogLevels, name)
}

// AddDevice adds a device reference of a specific category to the configuration
// e.g. a PV meter to site.PVs
func (c *Configure) AddDevice(d device, category DeviceCategory) {
switch DeviceCategories[category].class {
case DeviceClassCharger:
c.AddLogLevel(d.LogLevel)
if c.config.EEBUS != "" {
c.AddLogLevel("eebus")
}
c.config.Chargers = append(c.config.Chargers, d)
case DeviceClassMeter:
c.AddLogLevel(d.LogLevel)
c.config.Meters = append(c.config.Meters, d)
switch DeviceCategories[category].categoryFilter {
case DeviceCategoryGridMeter:
c.config.Site.Grid = d.Name
case DeviceCategoryPVMeter:
c.config.Site.PVs = append(c.config.Site.PVs, d.Name)
case DeviceCategoryBatteryMeter:
c.config.Site.Batteries = append(c.config.Site.Batteries, d.Name)
}
case DeviceClassVehicle:
c.AddLogLevel(d.LogLevel)
c.config.Vehicles = append(c.config.Vehicles, d)
}
}

// DevicesOfClass returns all configured devices of a given DeviceClass
func (c *Configure) DevicesOfClass(class DeviceClass) []device {
switch class {
case DeviceClassCharger:
return c.config.Chargers
case DeviceClassMeter:
return c.config.Meters
case DeviceClassVehicle:
return c.config.Vehicles
}
return nil
}

// AddLoadpoint adds a loadpoint to the configuration
func (c *Configure) AddLoadpoint(l loadpoint) {
c.config.Loadpoints = append(c.config.Loadpoints, l)
c.AddLogLevel(fmt.Sprintf("lp-%d", 1+len(c.config.Loadpoints)))
}

// MetersOfCategory returns the number of configured meters of a given DeviceCategory
func (c *Configure) MetersOfCategory(category DeviceCategory) int {
switch category {
case DeviceCategoryGridMeter:
if c.config.Site.Grid != "" {
return 1
}
case DeviceCategoryPVMeter:
return len(c.config.Site.PVs)
case DeviceCategoryBatteryMeter:
return len(c.config.Site.Batteries)
}

return 0
}

//go:embed configure.tpl
var configTmpl string

// RenderConfiguration creates a yaml configuration
func (c *Configure) RenderConfiguration() ([]byte, error) {
tmpl, err := template.New("yaml").Funcs(template.FuncMap(sprig.FuncMap())).Parse(configTmpl)
if err != nil {
panic(err)
}

out := new(bytes.Buffer)
err = tmpl.Execute(out, c.config)

return bytes.TrimSpace(out.Bytes()), err
}
Loading

0 comments on commit 7e41a97

Please sign in to comment.