cli configure updates (#1966)

- Add `--advanced` mode, where params defined as `advanced: true` will be asked
- Add support for handling user input for lists, e.g. `identifiers`
- Bool values are now a No/Yes selection instead of text input
- Common definitions (params and render) are moved into single `parambaselist.yaml` file
- Some refactoring
This commit is contained in:
Andreas Linde 2021-12-02 14:31:31 +01:00 • committed by GitHub
parent d538ec99f4
commit 4e119e8f6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 325 additions and 355 deletions

View file

@ -23,6 +23,7 @@ var acceptable = []string{
"can only have either uri or device", // modbus
"sponsorship required, see https://github.com/evcc-io/evcc#sponsorship",
"eebus not configured",
"unexpected status: 400", // easee
}
func TestConfigChargers(t *testing.T) {

View file

@ -19,6 +19,7 @@ var configureCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(configureCmd)
configureCmd.Flags().String("lang", "", "Define the localization to be used (en, de)")
configureCmd.Flags().Bool("advanced", false, "Enables handling of advanced configuration options")
configureCmd.Flags().Bool("expand", false, "Enables rendering expanded configuration files")
}
@ -30,6 +31,11 @@ func runConfigure(cmd *cobra.Command, args []string) {
log.FATAL.Fatal(err)
}
advanced, err := cmd.Flags().GetBool("advanced")
if err != nil {
panic(err)
}
expand, err := cmd.Flags().GetBool("expand")
if err != nil {
panic(err)
@ -37,5 +43,5 @@ func runConfigure(cmd *cobra.Command, args []string) {
util.LogLevel(viper.GetString("log"), nil)
impl.Run(log, lang, expand)
impl.Run(log, lang, advanced, expand)
}

View file

@ -11,7 +11,6 @@ import (
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
}

View file

@ -53,7 +53,7 @@ func (c *CmdConfigure) configureDeviceGuidedSetup() {
// we only ask for the configuration for the first usage
deviceCategory = supportedDeviceCategories[0]
values = c.processConfig(templateItem.Params, deviceCategory, false)
values = c.processConfig(templateItem.Params, deviceCategory)
deviceItem, err = c.processDeviceValues(values, templateItem, deviceItem, deviceCategory)
if err != nil {
@ -120,7 +120,7 @@ func (c *CmdConfigure) configureLinkedTypes(templateItem templates.Template) {
break
}
values := c.processConfig(linkedTemplateItem.Params, category, false)
values := c.processConfig(linkedTemplateItem.Params, category)
deviceItem, err = c.processDeviceValues(values, linkedTemplateItem, deviceItem, category)
if err != nil {
if !errors.Is(err, c.errDeviceNotValid) {
@ -164,7 +164,7 @@ func (c *CmdConfigure) configureDeviceCategory(deviceCategory DeviceCategory) (d
}
deviceDescription = templateItem.Description
values := c.processConfig(templateItem.Params, deviceCategory, false)
values := c.processConfig(templateItem.Params, deviceCategory)
device, err = c.processDeviceValues(values, templateItem, device, deviceCategory)
if err != nil {

View file

@ -32,7 +32,6 @@ func (c *CmdConfigure) processDeviceValues(values map[string]interface{}, templa
device.Name = fmt.Sprintf("%s%d", DeviceCategories[deviceCategory].defaultName, c.addedDeviceIndex)
device.Title = templateItem.Description
device.LogLevel = templateItem.LogLevel
for item, value := range values {
if strings.ToLower(item) != "title" {
continue
@ -233,120 +232,158 @@ func (c *CmdConfigure) paramChoiceValues(params []templates.Param, name string)
// processConfig processes an EVCC configuration item
// Returns:
// a map with param name and values
func (c *CmdConfigure) processConfig(paramItems []templates.Param, deviceCategory DeviceCategory, includeAdvanced bool) map[string]interface{} {
func (c *CmdConfigure) processConfig(paramItems []templates.Param, deviceCategory DeviceCategory) map[string]interface{} {
usageFilter := DeviceCategories[deviceCategory].categoryFilter
additionalConfig := make(map[string]interface{})
selectedModbusKey := ""
fmt.Println()
fmt.Println(c.localizedString("Config_Title", nil))
fmt.Println()
for _, param := range paramItems {
if param.Name == templates.ParamModbus {
// baudrate and comset defaults can be overwritten, as they are device specific
deviceDefaultBaudrate := templates.ModbusParamValueBaudrate
if param.Baudrate != 0 {
deviceDefaultBaudrate = param.Baudrate
switch param.Name {
case templates.ParamModbus:
c.processModbusConfig(param, deviceCategory, additionalConfig)
case templates.ParamUsage:
if usageFilter != "" {
additionalConfig[param.Name] = usageFilter.String()
}
deviceDefaultComset := templates.ModbusParamValueComset
if param.Comset != "" {
deviceDefaultBaudrate = param.Baudrate
}
choices := []string{}
choiceKeys := []string{}
for _, choice := range param.Choice {
switch choice {
case templates.ModbusChoiceRS485:
choices = append(choices, "Serial (USB-RS485 Adapter)")
choiceKeys = append(choiceKeys, templates.ModbusKeyRS485Serial)
choices = append(choices, "Serial (Ethernet-RS485 Adapter)")
choiceKeys = append(choiceKeys, templates.ModbusKeyRS485TCPIP)
case templates.ModbusChoiceTCPIP:
choices = append(choices, "TCP/IP")
choiceKeys = append(choiceKeys, templates.ModbusKeyTCPIP)
}
}
if len(choices) > 0 {
// ask for modbus address
id := c.askValue(question{
label: "ID",
help: "Modbus ID",
defaultValue: 1,
valueType: templates.ParamValueTypeNumber,
required: true})
additionalConfig[templates.ModbusParamNameId] = id
// ask for modbus interface type
index := 0
if len(choices) > 1 {
index, _ = c.askChoice(c.localizedString("Config_ModbusInterface", nil), choices)
}
selectedModbusKey = choiceKeys[index]
switch selectedModbusKey {
case templates.ModbusKeyRS485Serial:
device := c.askValue(question{
label: "Device",
help: "USB-RS485 Adapter Adresse",
exampleValue: templates.ModbusParamValueDevice,
required: true})
additionalConfig[templates.ModbusParamNameDevice] = device
baudrate := c.askValue(question{
label: "Baudrate",
defaultValue: deviceDefaultBaudrate,
valueType: templates.ParamValueTypeNumber,
required: true})
additionalConfig[templates.ModbusParamNameBaudrate] = baudrate
comset := c.askValue(question{
label: "ComSet",
defaultValue: deviceDefaultComset,
required: true})
additionalConfig[templates.ModbusParamNameComset] = comset
case templates.ModbusKeyRS485TCPIP, templates.ModbusKeyTCPIP:
if selectedModbusKey == templates.ModbusKeyRS485TCPIP {
additionalConfig[templates.ModbusParamNameRTU] = "true"
}
host := c.askValue(question{
label: "Host",
exampleValue: templates.ModbusParamValueHost,
required: true})
additionalConfig[templates.ModbusParamNameHost] = host
port := c.askValue(question{
label: "Port",
defaultValue: templates.ModbusParamValuePort,
valueType: templates.ParamValueTypeNumber,
required: true})
additionalConfig[templates.ModbusParamNamePort] = port
}
}
} else if param.Name != templates.ParamUsage {
if !includeAdvanced && param.Advanced {
default:
if !c.advancedMode && param.Advanced {
continue
}
userFriendly := c.userFriendlyTexts(param)
value := c.askValue(question{
label: userFriendly.Name,
defaultValue: userFriendly.Default,
exampleValue: userFriendly.Example,
help: userFriendly.Help.String(c.lang),
valueType: userFriendly.ValueType,
mask: userFriendly.Mask,
required: userFriendly.Required})
additionalConfig[param.Name] = value
} else if param.Name == templates.ParamUsage {
if usageFilter != "" {
additionalConfig[param.Name] = usageFilter.String()
switch param.ValueType {
case templates.ParamValueTypeStringList:
additionalConfig[param.Name] = c.processListInputConfig(param)
default:
additionalConfig[param.Name] = c.processInputConfig(param)
}
}
}
return additionalConfig
}
// handle user input of multiple items in a list
func (c *CmdConfigure) processListInputConfig(param templates.Param) []string {
var values []string
// ask for values until the decides stops
for ok := true; ok; {
newValue := c.processInputConfig(param)
values = append(values, newValue)
if newValue == "" {
break
}
if !c.askYesNo(" " + c.localizedString("Config_AddAnotherValue", nil)) {
break
}
}
return values
}
// handle user input for a simple one value input
func (c *CmdConfigure) processInputConfig(param templates.Param) string {
userFriendly := c.userFriendlyTexts(param)
return c.askValue(question{
label: userFriendly.Name,
defaultValue: userFriendly.Default,
exampleValue: userFriendly.Example,
help: userFriendly.Help.String(c.lang),
valueType: userFriendly.ValueType,
mask: userFriendly.Mask,
required: userFriendly.Required})
}
// handle user input for a device modbus configuration
func (c *CmdConfigure) processModbusConfig(param templates.Param, deviceCategory DeviceCategory, additionalConfig map[string]interface{}) {
var selectedModbusKey string
// baudrate and comset defaults can be overwritten, as they are device specific
deviceDefaultBaudrate := templates.ModbusParamValueBaudrate
if param.Baudrate != 0 {
deviceDefaultBaudrate = param.Baudrate
}
deviceDefaultComset := templates.ModbusParamValueComset
if param.Comset != "" {
deviceDefaultBaudrate = param.Baudrate
}
var choices []string
var choiceKeys []string
for _, choice := range param.Choice {
switch choice {
case templates.ModbusChoiceRS485:
choices = append(choices, "Serial (USB-RS485 Adapter)")
choiceKeys = append(choiceKeys, templates.ModbusKeyRS485Serial)
choices = append(choices, "Serial (Ethernet-RS485 Adapter)")
choiceKeys = append(choiceKeys, templates.ModbusKeyRS485TCPIP)
case templates.ModbusChoiceTCPIP:
choices = append(choices, "TCP/IP")
choiceKeys = append(choiceKeys, templates.ModbusKeyTCPIP)
}
}
if len(choices) > 0 {
// ask for modbus address
id := c.askValue(question{
label: "ID",
help: "Modbus ID",
defaultValue: 1,
valueType: templates.ParamValueTypeNumber,
required: true})
additionalConfig[templates.ModbusParamNameId] = id
// ask for modbus interface type
var index int
if len(choices) > 1 {
index, _ = c.askChoice(c.localizedString("Config_ModbusInterface", nil), choices)
}
selectedModbusKey = choiceKeys[index]
switch selectedModbusKey {
case templates.ModbusKeyRS485Serial:
device := c.askValue(question{
label: "Device",
help: "USB-RS485 Adapter Adresse",
exampleValue: templates.ModbusParamValueDevice,
required: true})
additionalConfig[templates.ModbusParamNameDevice] = device
baudrate := c.askValue(question{
label: "Baudrate",
defaultValue: deviceDefaultBaudrate,
valueType: templates.ParamValueTypeNumber,
required: true})
additionalConfig[templates.ModbusParamNameBaudrate] = baudrate
comset := c.askValue(question{
label: "ComSet",
defaultValue: deviceDefaultComset,
required: true})
additionalConfig[templates.ModbusParamNameComset] = comset
case templates.ModbusKeyRS485TCPIP, templates.ModbusKeyTCPIP:
if selectedModbusKey == templates.ModbusKeyRS485TCPIP {
additionalConfig[templates.ModbusParamNameRTU] = "true"
}
host := c.askValue(question{
label: "Host",
exampleValue: templates.ModbusParamValueHost,
required: true})
additionalConfig[templates.ModbusParamNameHost] = host
port := c.askValue(question{
label: "Port",
defaultValue: templates.ModbusParamValuePort,
valueType: templates.ParamValueTypeNumber,
required: true})
additionalConfig[templates.ModbusParamNamePort] = port
}
}
}

View file

@ -75,6 +75,9 @@ Requirements_EEBUS_Pairing = "Du hast eine Wallbox ausgewählt, welche über das
Config_Title = "Führe folgende Einstellungen durch:"
Config_ModbusInterface = "Wähle die ModBus Schnittstelle aus"
Config_AddAnotherValue = "Möchtest du einen weiteren Wert hinzufügen?"
Config_Yes = "Ja"
Config_No = "Nein"
Cancel = "Die Konfiguration wurde abgebrochen.\n\nFalls diese geführte Konfiguration für dich noch nicht funktioniert, versuche es doch mal mit der manuellen Konfiguration. Details findest du auf der folgenden Webseite: https://docs.evcc.io/docs/installation/configuration\n"
InputError = "Bei der Eingabe ist ein Fehler aufgetreten:"
@ -82,10 +85,8 @@ InputError = "Bei der Eingabe ist ein Fehler aufgetreten:"
Value_Required = "erforderlich"
Value_Optional = "optional"
Value_Sample = "Beispiel"
Value_Bool = "'true' für ja oder 'false' für nein"
ValueError_Used = "Dieser Wert wird bereits verwendet."
ValueError_Empty = "Der Wert darf nicht leer sein."
ValueError_Bool = "Der Wert muss 'true' (für ja) oder 'false' (für nein) sein."
ValueError_Float = "Der Wert muss eine Zahl sein. Nachkommastellen mit . anstatt mit , trennen."
ValueError_Number = "Der Wert muss eine ganzzahlige Zahl sein."

View file

@ -75,6 +75,9 @@ Requirements_EEBUS_Pairing = "You selected a wallbox, which will be access via t
Config_Title = "Please provide the following settings:"
Config_ModbusInterface = "Choose the ModBus interface"
Config_AddAnotherValue = "Do you want to add another value?"
Config_Yes = "Yes"
Config_No = "No"
Cancel = "The configuration was cancelled.\n\nIf this guided configuration process doesn't work for you yet, please try the manual configuration. You can find more details about that on our website: https://docs.evcc.io/docs/installation/configuration\n"
InputError = "An input error occurred:"
@ -82,10 +85,8 @@ InputError = "An input error occurred:"
Value_Required = "required"
Value_Optional = "optional"
Value_Sample = "Example"
Value_Bool = "'true' for yes or 'false' for no"
ValueError_Used = "This value is aready in use."
ValueError_Empty = "The value may not be empty."
ValueError_Bool = "The value has to be 'true' (for yes) or 'false' (for no)."
ValueError_Float = "The value has to be a number."
ValueError_Number = "The value has to be an integer."

View file

@ -29,14 +29,15 @@ type CmdConfigure struct {
log *util.Logger
lang string
expandedMode bool
advancedMode, expandedMode bool
addedDeviceIndex int
errItemNotPresent, errDeviceNotValid error
}
// Run starts the interactive configuration
func (c *CmdConfigure) Run(log *util.Logger, flagLang string, expandedMode bool) {
func (c *CmdConfigure) Run(log *util.Logger, flagLang string, advancedMode, expandedMode bool) {
c.log = log
c.advancedMode = advancedMode
c.expandedMode = expandedMode
c.log.INFO.Printf("evcc %s (%s)", server.Version, server.Commit)

View file

@ -5,7 +5,6 @@ import (
"fmt"
"os"
"strconv"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
@ -118,8 +117,26 @@ type question struct {
mask, required bool
}
// askBoolValue asks for a boolean value selection for a given question
func (c *CmdConfigure) askBoolValue(label string) string {
choices := []string{c.localizedString("Config_No", nil), c.localizedString("Config_Yes", nil)}
values := []string{"false", "true"}
index, _ := c.askChoice(label, choices)
return values[index]
}
// askValue asks for value input for a given question (template param)
func (c *CmdConfigure) askValue(q question) string {
if q.valueType == templates.ParamValueTypeBool {
label := q.label
if q.help != "" {
label = q.help
}
return c.askBoolValue(label)
}
input := ""
var err error
@ -134,12 +151,6 @@ func (c *CmdConfigure) askValue(q question) string {
return errors.New(c.localizedString("ValueError_Empty", nil))
}
if q.valueType == templates.ParamValueTypeBool {
if strings.ToLower(value) != "true" && strings.ToLower(value) != "false" {
return errors.New(c.localizedString("ValueError_Bool", nil))
}
}
if q.valueType == templates.ParamValueTypeFloat {
_, err := strconv.ParseFloat(value, 64)
if err != nil {
@ -166,9 +177,6 @@ func (c *CmdConfigure) askValue(q question) string {
if q.exampleValue != "" {
help += fmt.Sprintf(" ("+c.localizedString("Value_Sample", nil)+": %s)", q.exampleValue)
}
if q.valueType == templates.ParamValueTypeBool {
help += " (" + c.localizedString("Value_Bool", nil) + ")"
}
if q.mask {
prompt := &survey.Password{

View file

@ -81,7 +81,7 @@ func (c *CmdConfigure) userFriendlyTexts(param templates.Param) templates.Param
if result.Help.String(c.lang) == "" {
result.Help.SetString(c.lang, c.localizedString("UserFriendly_Vin_Help", nil))
}
case "identifier":
case "identifiers":
result.Name = c.localizedString("UserFriendly_Identifier_Name", nil)
if result.Help.String(c.lang) == "" {
result.Help.SetString(c.lang, c.localizedString("UserFriendly_Identifier_Help", nil))

View file

@ -112,9 +112,14 @@ Allows to define a list of meter devices that are typically installed with this
**Possible values**:
- `string`: for string values (default)
- `bool`: for `true` and `false` values
- `bool`: for `true` and `false` values. If `help` is provided, than that help text is presented as the question
- `number`: for int values
- `float`: for float values
- `stringlist`: for a list of strings, e.g.used for defining a list of `identifiers` for `vehicles`
### `advanced`
`advanced` allows to specify if the param should only be asked if the cli is run with `--advanced`. Mostly used for non required params that are meant for users with advanced needs and knowledge.
### `help`

View file

@ -1,6 +1,5 @@
template: pmcc
description: Porsche Mobile Charger Connect
loglevel: eebus
requirements:
eebus: true
params:

View file

@ -1,6 +1,5 @@
template: pmcp
description: Porsche Mobile Charger Plus
loglevel: eebus
requirements:
eebus: true
params:

View file

@ -5,4 +5,7 @@ import "embed"
var (
//go:embed charger/*.yaml meter/*.yaml vehicle/*.yaml
YamlTemplates embed.FS
//go:embed parambaselist.yaml
ParamBaseListDefinition string
)

View file

@ -2,7 +2,6 @@ template: sma-home-manager
description: SMA Sunny Home Manager 2.0
requirements:
hems: sma
loglevel: sma
guidedsetup:
enable: true
linked:

View file

@ -0,0 +1,36 @@
vehicle:
params:
- name: title
- name: user
required: true
- name: password
required: true
mask: true
- name: vin
example: W...
- name: capacity
default: '50'
valueType: float
- name: identifiers
advanced: true
valueType: stringlist
render: |
{{define "vehicle-common"}}
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
{{- if ne .capacity "" }}
capacity: {{ .capacity }}
{{- end }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{- if ne (len .identifiers) 0 }}
identifiers:
{{- range .identifiers }}
- {{ . }}
{{- end }}
{{- end }}
{{end}}

View file

@ -8,12 +8,4 @@ params:
default: 60
render: |
type: audi
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -8,12 +8,4 @@ params:
default: 65
render: |
type: bmw
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,12 +6,4 @@ params:
default: 50
render: |
type: carwings
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,12 +6,4 @@ params:
default: 50
render: |
type: citroen
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,12 +6,4 @@ params:
default: 50
render: |
type: ds
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -9,15 +9,7 @@ params:
default: 42
render: |
type: fiat
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}
{{- if ne .pin "" }}
pin: {{ .pin }} #mandatory to deep refresh SoC
{{- end }}

View file

@ -8,12 +8,4 @@ params:
default: 10
render: |
type: ford
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,9 +6,4 @@ params:
default: 64
render: |
type: hyundai
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{include "vehicle-common" .}}

View file

@ -6,9 +6,4 @@ params:
default: 64
render: |
type: kia
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{include "vehicle-common" .}}

View file

@ -8,12 +8,4 @@ params:
default: 32
render: |
type: mini
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,9 +6,4 @@ params:
default: 60
render: |
type: nissan
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{include "vehicle-common" .}}

View file

@ -8,12 +8,4 @@ params:
default: 50
render: |
type: opel
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,12 +6,4 @@ params:
default: 50
render: |
type: peugeot
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -1,20 +1,9 @@
template: porsche
description: Porsche
loglevel: porsche
paramsbase: vehicle
params:
- name: capacity
default: 83.4
render: |
type: porsche
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
{{- if .capacity }}
capacity: {{ .capacity }}
{{- end }}
{{- if .vin }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -8,12 +8,4 @@ params:
default: 60
render: |
type: renault
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
capacity: {{ .capacity }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,14 +6,4 @@ params:
default: 10
render: |
type: seat
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
{{- if ne .capacity "" }}
capacity: {{ .capacity }}
{{- end }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,14 +6,4 @@ params:
default: 10
render: |
type: skoda
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
{{- if ne .capacity "" }}
capacity: {{ .capacity }}
{{- end }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,14 +6,4 @@ params:
default: 50
render: |
type: enyaq
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
{{- if ne .capacity "" }}
capacity: {{ .capacity }}
{{- end }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -6,14 +6,4 @@ params:
default: 50
render: |
type: volvo
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
{{- if ne .capacity "" }}
capacity: {{ .capacity }}
{{- end }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -8,14 +8,4 @@ params:
default: 10
render: |
type: vw
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
{{- if ne .capacity "" }}
capacity: {{ .capacity }}
{{- end }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -1,6 +1,5 @@
template: id
description: Volkswagen (We Connect ID)
loglevel: id
paramsbase: vehicle
params:
- name: vin
@ -9,14 +8,4 @@ params:
default: 50
render: |
type: id
{{- if ne .title "" }}
title: {{ .title }}
{{- end }}
user: {{ .user }}
password: {{ .password }}
{{- if ne .capacity "" }}
capacity: {{ .capacity }}
{{- end }}
{{- if ne .vin "" }}
vin: {{ .vin }}
{{- end }}
{{include "vehicle-common" .}}

View file

@ -7,4 +7,9 @@ description: {{ .Description }}
{{ .Name }}:
{{- if len .Value }} {{ .Value }} {{ end }}
{{- if .Help.DE }} # {{ .Help.DE }} {{- end }}
{{- if ne (len .Values) 0 }}
{{- range .Values }}
- {{ . }}
{{- end }}
{{- end }}
{{ end -}}

View file

@ -4,10 +4,13 @@ import (
"bytes"
_ "embed"
"fmt"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
"github.com/evcc-io/evcc/templates/definition"
"github.com/evcc-io/evcc/util"
"gopkg.in/yaml.v3"
)
const (
@ -45,10 +48,11 @@ const (
var HemsValueTypes = []string{HemsTypeSMA}
const (
ParamValueTypeString = "string"
ParamValueTypeNumber = "number"
ParamValueTypeFloat = "float"
ParamValueTypeBool = "bool"
ParamValueTypeString = "string"
ParamValueTypeNumber = "number"
ParamValueTypeFloat = "float"
ParamValueTypeBool = "bool"
ParamValueTypeStringList = "stringlist"
)
var ParamValueTypes = []string{ParamValueTypeString, ParamValueTypeNumber, ParamValueTypeBool}
@ -111,6 +115,7 @@ type Param struct {
Help TextLanguage // cli configuration help
Test string // testing default value
Value string // user provided value via cli configuration
Values []string // user provided list of values
ValueType string // string representation of the value type, "string" is default
Choice []string // defines which usage choices this config supports, valid elemtents are "grid", "pv", "battery", "charge"
Usages []string
@ -118,11 +123,17 @@ type Param struct {
Comset string // device specific default for modbus RS485 comset
}
type ParamBase struct {
Params []Param
Render string
}
var paramBaseList map[string]ParamBase
// Template describes is a proxy device for use with cli and automated testing
type Template struct {
Template string
Description string // user friendly description of the device this template describes
LogLevel string // the implementation type of the device, equal to the type value under "Render"
Requirements Requirements
GuidedSetup GuidedSetup
Generic bool // if this describes a generic device type rather than a product
@ -131,31 +142,30 @@ type Template struct {
Render string // rendering template
}
var paramBases = map[string][]Param{
"vehicle": {
{Name: "title"},
{Name: "user", Required: true},
{Name: "password", Required: true, Mask: true},
{Name: "vin", Example: "W..."},
{Name: "capacity", Default: "50", ValueType: ParamValueTypeFloat},
},
}
// add the referenced base Params and overwrite existing ones
func (t *Template) ResolveParamBase() {
if t.ParamsBase == "" {
return
}
base, ok := paramBases[t.ParamsBase]
if paramBaseList == nil {
err := yaml.Unmarshal([]byte(definition.ParamBaseListDefinition), &paramBaseList)
if err != nil {
fmt.Printf("Error: failed to parse paramBasesDefinition: %v\n", err)
return
}
}
base, ok := paramBaseList[t.ParamsBase]
if !ok {
fmt.Printf("Error: Could not find parambase definition: %s\n", t.ParamsBase)
return
}
currentParams := make([]Param, len(t.Params))
copy(currentParams, t.Params)
t.Params = make([]Param, len(base))
copy(t.Params, base)
t.Params = make([]Param, len(base.Params))
copy(t.Params, base.Params)
for _, p := range currentParams {
if i, item := t.paramWithName(p.Name); item != nil {
// we only allow overwriting a few fields
@ -175,12 +185,17 @@ func (t *Template) ResolveParamBase() {
func (t *Template) Defaults(docsOrTests bool) map[string]interface{} {
values := make(map[string]interface{})
for _, p := range t.Params {
if p.Test != "" {
values[p.Name] = p.Test
} else if p.Example != "" && docsOrTests {
values[p.Name] = p.Example
} else {
values[p.Name] = p.Default // may be empty
switch p.ValueType {
case ParamValueTypeStringList:
values[p.Name] = []string{}
default:
if p.Test != "" {
values[p.Name] = p.Test
} else if p.Example != "" && docsOrTests {
values[p.Name] = p.Example
} else {
values[p.Name] = p.Default // may be empty
}
}
}
@ -230,25 +245,39 @@ func (t *Template) RenderProxyWithValues(values map[string]interface{}, includeD
for index, p := range t.Params {
for k, v := range values {
if p.Name == k {
t.Params[index].Value = v.(string)
if p.Name != k {
continue
}
switch p.ValueType {
case ParamValueTypeStringList:
for _, e := range v.([]string) {
t.Params[index].Values = append(p.Values, yamlQuote(e))
}
default:
t.Params[index].Value = yamlQuote(v.(string))
}
}
}
// remove params with no values, no defaults and no example
// remove params with no values
var newParams []Param
for _, param := range t.Params {
if param.Value == "" && param.Default == "" && param.Example == "" && !param.Required {
continue
if !param.Required {
switch param.ValueType {
case ParamValueTypeStringList:
if len(param.Values) == 0 {
continue
}
default:
if param.Value == "" {
continue
}
}
}
newParams = append(newParams, param)
}
for index, p := range newParams {
newParams[index].Value = yamlQuote(p.Value)
}
t.Params = newParams
out := new(bytes.Buffer)
@ -273,8 +302,24 @@ func (t *Template) RenderResult(docs bool, other map[string]interface{}) ([]byte
t.ModbusValues(values)
// add the common templates
for _, v := range paramBaseList {
if !strings.Contains(t.Render, v.Render) {
t.Render = fmt.Sprintf("%s\n%s", t.Render, v.Render)
}
}
for item, p := range values {
values[item] = yamlQuote(fmt.Sprintf("%v", p))
switch p := p.(type) {
case []string:
var list []string
for _, v := range p {
list = append(list, yamlQuote(v))
}
values[item] = list
default:
values[item] = yamlQuote(fmt.Sprintf("%v", p))
}
}
tmpl := template.New("yaml")