diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go index 990ee5985b..d8f35c6226 100644 --- a/cmd/configure/configure.go +++ b/cmd/configure/configure.go @@ -16,14 +16,15 @@ type device struct { } 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 + 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 + ResetOnDisconnect string } type config struct { diff --git a/cmd/configure/configure.tpl b/cmd/configure/configure.tpl index 08df387d2c..49d1be8f8c 100644 --- a/cmd/configure/configure.tpl +++ b/cmd/configure/configure.tpl @@ -43,6 +43,7 @@ loadpoints: phases: {{ .Phases }} mincurrent: {{ .MinCurrent }} maxcurrent: {{ .MaxCurrent }} + resetOnDisconnect: {{ .ResetOnDisconnect }} {{- end }} {{- end }} diff --git a/cmd/configure/helper.go b/cmd/configure/helper.go index d931c183e8..cb899b37da 100644 --- a/cmd/configure/helper.go +++ b/cmd/configure/helper.go @@ -307,12 +307,21 @@ func (c *CmdConfigure) processModbusConfig(param templates.Param, deviceCategory // baudrate and comset defaults can be overwritten, as they are device specific deviceDefaultBaudrate := templates.ModbusParamValueBaudrate + deviceDefaultComset := templates.ModbusParamValueComset + deviceDefaultPort := templates.ModbusParamValuePort + deviceDefaultId := templates.ModbusParamValueId + if param.Baudrate != 0 { deviceDefaultBaudrate = param.Baudrate } - deviceDefaultComset := templates.ModbusParamValueComset if param.Comset != "" { - deviceDefaultBaudrate = param.Baudrate + deviceDefaultComset = param.Comset + } + if param.Port != 0 { + deviceDefaultPort = param.Port + } + if param.ID != 0 { + deviceDefaultId = param.ID } var choices []string @@ -336,7 +345,7 @@ func (c *CmdConfigure) processModbusConfig(param templates.Param, deviceCategory id := c.askValue(question{ label: "ID", help: "Modbus ID", - defaultValue: 1, + defaultValue: deviceDefaultId, valueType: templates.ParamValueTypeNumber, required: true}) additionalConfig[templates.ModbusParamNameId] = id @@ -383,7 +392,7 @@ func (c *CmdConfigure) processModbusConfig(param templates.Param, deviceCategory port := c.askValue(question{ label: c.localizedString("UserFriendly_Port_Name", nil), - defaultValue: templates.ModbusParamValuePort, + defaultValue: deviceDefaultPort, valueType: templates.ParamValueTypeNumber, required: true}) additionalConfig[templates.ModbusParamNamePort] = port diff --git a/cmd/configure/localization/de.toml b/cmd/configure/localization/de.toml index 9310bd6cf5..961d4a1b4a 100644 --- a/cmd/configure/localization/de.toml +++ b/cmd/configure/localization/de.toml @@ -26,6 +26,7 @@ Error_EEBUS_Certificate_Create = "Konnte das EEBUS Zertifikat nicht erstellen" Error_EEBUS_Certificate_Use = "Konnte das EEBUS Zertifikat nicht verwenden" File_Exists = "Die Datei {{ .FileName }} existiert bereits. Soll die Datei überschrieben werden?" +File_Permissions = "Die Datei {{ .FileName }} existiert bereits und kann nicht überschrieben werden." File_NewFilename = "Bitte gib einen neuen Dateinamen an" File_Error_SaveFailed = "Die Konfiguration konnte nicht in der Datei {{ .FileName }} gespeichert werden" File_SaveSuccess = "Die Konfiguration wurde erfolgreich in der Datei {{ .FileName }} gespeichert" @@ -45,6 +46,16 @@ UserFriendly_Password_Name = "Passwort" UserFriendly_Capacity_Name = "Akku-Kapazität in kWh" UserFriendly_Vin_Name = "Fahrzeugidentifikationsnummer" UserFriendly_Vin_Help = "Erforderlich, wenn mehrere Fahrzeuge des Herstellers vorhanden sind" +UserFriendly_Cache_Name = "Cache" +UserFriendly_Cache_Help = "Das zeitliche Interval, in welchem Daten vom Fahrzeug neu geladen werden soll" +UserFriendly_MinSoC_Name = "Minimaler Ladestand (SoC) in %" +UserFriendly_MinSoC_Help = "Lade sofort mit maximaler Geschwindigkeit bis zu dem angegeben Ladestand, wenn der Lademodus nicht auf 'Aus' steht" +UserFriendly_TargetSoC_Name = "Ziel-Ladestand (SoC) in %" +UserFriendly_TargetSoC_Help = "Bis zu welchem Ladestand (SoC) soll das Fahrzeug geladen werden" +UserFriendly_MinCurrent_Name = "Minimale Stromstärke in Ampere (A)" +UserFriendly_MinCurrent_Help = "Definiert die minimale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll" +UserFriendly_MaxCurrent_Name = "Maximale Stromstärke in Ampere (A)" +UserFriendly_MaxCurrent_Help = "Definiert die maximale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll" UserFriendly_Identifier_Name = "Identifikationsnummer" UserFriendly_Identifier_Help = "Kann meist erst später eingetragen werden, siehe: https://docs.evcc.io/docs/guides/vehicles/#erkennung-des-fahrzeugs-an-der-wallbox" UserFriendly_StandByPower_Name = "Standby-Leistung in W" @@ -113,6 +124,8 @@ Loadpoint_Title = "Titel des Ladepunktes" Loadpoint_AddAnother = "Möchtest du einen weiteren Ladepunkt hinzufügen?" Loadpoint_DefaultTitle = "Garage" +Loadpoint_ResetOnDisconnect = "Soll beim Abstecken des Ladekables von einem Fahrzeug, die Lade-Standardeinstellungen wieder hergestellt werden?" + Loadpoint_WallboxWOMeter = "Die Wallbox hat keinen Ladestromzähler. Hast du einen externen Zähler dafür installiert, der verwendet werden kann?" Loadpoint_WallboxMaxPower = "Was ist die maximale Leistung, welche die Wallbox zur Verfügung stellen kann?" Loadpoint_WallboxMaxAmperage = "Was ist die maximale Stromstärke, welche die Wallbox auf einer Phase zur Verfügung stellen kann?" @@ -124,11 +137,12 @@ Loadpoint_WallboxPowerOther = "Andere Leistung" Loadpoint_VehicleChargeHere = "Wird das Fahrzeug {{ .Vehicle }} hier laden?" -Loadpoint_DefaultChargeMode = "Was sollte der Standard-Lademodus sein, wenn ein Fahrzeug angeschlossen wird?" -Loadpoint_ChargeModeOff = "Aus" -Loadpoint_ChargeModeNow = "Sofort (mit größtmöglicher Leistung)" -Loadpoint_ChargeModeMinPV = "Min+PV (mit der kleinstmöglichen Leistung, schneller wenn genügend PV Überschuss vorhanden ist)" -Loadpoint_ChargeModePV = "PV (Nur mit PV Überschuss)" +ChargeMode_Question = "Was sollte der Standard-Lademodus sein, wenn ein Fahrzeug angeschlossen wird?" +ChargeModeOff = "Stop" +ChargeModeNow = "Sofort (mit größtmöglicher Leistung)" +ChargeModeMinPV = "Min+PV (mit der kleinstmöglichen Leistung, schneller wenn genügend PV Überschuss vorhanden ist)" +ChargeModePV = "PV (Nur mit PV Überschuss)" +ChargeModeNone = "Keinen (Benutzt den im Ladepunkt gesetzten Lademodus)" Site_Setup = "- Standort einrichten" Site_Title = "Titel des Standortes" diff --git a/cmd/configure/localization/en.toml b/cmd/configure/localization/en.toml index d523edfd76..6dc712e588 100644 --- a/cmd/configure/localization/en.toml +++ b/cmd/configure/localization/en.toml @@ -26,6 +26,7 @@ Error_EEBUS_Certificate_Create = "Could not create the EEBUS certificate" Error_EEBUS_Certificate_Use = "Could not process teh generated EEBUS certificate" File_Exists = "The file {{ .FileName }} already exists. Do you want to replace it?" +File_Permissions = "The file {{ .FileName }} already exists and can not be overwritten." File_NewFilename = "Please provide a new filename" File_Error_SaveFailed = "The configuration could not be saved in the file {{ .FileName }}" File_SaveSuccess = "The configuration was successfully saved in the file {{ .FileName }}" @@ -45,6 +46,16 @@ UserFriendly_Password_Name = "Password" UserFriendly_Capacity_Name = "Battery-Capacity in kWh" UserFriendly_Vin_Name = "Vehicle Identification Number" UserFriendly_Vin_Help = "Required if you own multiple vehicles of the same brand" +UserFriendly_Cache_Name = "Cache" +UserFriendly_Cache_Help = "Time interval with when data should be reloaded from the vehicle" +UserFriendly_MinSoC_Name = "Minimum state of charge (SoC) in %" +UserFriendly_MinSoC_Help = "Charge immediately with maximum power up to the defined state of charge, if the charge mode is not set to 'OFF'" +UserFriendly_TargetSoC_Name = "Target state of charge (SoC) in %" +UserFriendly_TargetSoC_Help = "Until which sate of charge (SoC) should the vehicle be charged" +UserFriendly_MinCurrent_Name = "Minimum amperage (A)" +UserFriendly_MinCurrent_Help = "The minimum amperage per connected phase with which the car should be charged" +UserFriendly_MaxCurrent_Name = "Maximum amperage (A)" +UserFriendly_MaxCurrent_Help = "The maximum amperage per connected phase with which the car shuold be charged" UserFriendly_Identifier_Name = "Identification" UserFriendly_Identifier_Help = "Mostly this can be added later, see: https://docs.evcc.io/docs/guides/vehicles/#erkennung-des-fahrzeugs-an-der-wallbox" UserFriendly_StandByPower_Name = "Standby power in W" @@ -113,6 +124,8 @@ Loadpoint_Title = "Loadpoint title" Loadpoint_AddAnother = "Do you want to add another loadpoint?" Loadpoint_DefaultTitle = "Garage" +Loadpoint_ResetOnDisconnect = "Shall disconnecting the charging cable from the vehicle reset the charging settings to the defaults?" + Loadpoint_WallboxWOMeter = "The wallbox does not provide power data. Do you have an external meter that can be used?" Loadpoint_WallboxMaxPower = "What is the maximum power your wallbox can provide?" Loadpoint_WallboxMaxAmperage = "What is the maximum amperage your wallbox can provide on a single phase?" @@ -124,11 +137,12 @@ Loadpoint_WallboxPowerOther = "Other option" Loadpoint_VehicleChargeHere = "Will the vehicle {{ .Vehicle }} be charging here?" -Loadpoint_DefaultChargeMode = "Was sollte der Standard-Lademodus sein, wenn ein Fahrzeug angeschlossen wird?" -Loadpoint_ChargeModeOff = "Off" -Loadpoint_ChargeModeNow = "Now (charging with maximum power)" -Loadpoint_ChargeModeMinPV = "Min+PV (charging with minimum power, faster if enough PV power surplus is available)" -Loadpoint_ChargeModePV = "PV (Only with PV power surplus)" +ChargeMode_Question = "What should be the default charging mode when a vehicle is connected?" +ChargeModeOff = "Off" +ChargeModeNow = "Now (charging with maximum power)" +ChargeModeMinPV = "Min+PV (charging with minimum power, faster if enough PV power surplus is available)" +ChargeModePV = "PV (Only with PV power surplus)" +ChargeModeNone = "None (Use what is set at the loadpoint)" Site_Setup = "- Setup site" Site_Title = "Site title" diff --git a/cmd/configure/main.go b/cmd/configure/main.go index ec89874a24..567971da5a 100644 --- a/cmd/configure/main.go +++ b/cmd/configure/main.go @@ -11,7 +11,6 @@ import ( "github.com/BurntSushi/toml" "github.com/cloudfoundry/jibber_jabber" - "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/server" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/templates" @@ -149,13 +148,18 @@ func (c *CmdConfigure) flowNewConfigFile() { filename := DefaultConfigFilename for ok := true; ok; { - _, err := os.Open(filename) + file, err := os.OpenFile(filename, os.O_WRONLY, 0666) if errors.Is(err, os.ErrNotExist) { break } - - if c.askYesNo(c.localizedString("File_Exists", localizeMap{"FileName": filename})) { - break + file.Close() + // in case of permission error, we can't write to the file anyway + if os.IsPermission(err) { + fmt.Println(c.localizedString("File_Permissions", localizeMap{"FileName": filename})) + } else { + if c.askYesNo(c.localizedString("File_Exists", localizeMap{"FileName": filename})) { + break + } } filename = c.askValue(question{ @@ -302,17 +306,14 @@ func (c *CmdConfigure) configureLoadpoints() { } } - chargingModes := []string{string(api.ModeOff), string(api.ModeNow), string(api.ModeMinPV), string(api.ModePV)} - chargeModes := []string{ - c.localizedString("Loadpoint_ChargeModeOff", nil), - c.localizedString("Loadpoint_ChargeModeNow", nil), - c.localizedString("Loadpoint_ChargeModeMinPV", nil), - c.localizedString("Loadpoint_ChargeModePV", nil), - } fmt.Println() - modeChoice, _ := c.askChoice(c.localizedString("Loadpoint_DefaultChargeMode", nil), chargeModes) - loadpoint.Mode = chargingModes[modeChoice] + loadpoint.Mode = c.askValue(question{valueType: templates.ParamValueTypeChargeModes, excludeNone: true}) + fmt.Println() + loadpoint.ResetOnDisconnect = c.askValue(question{ + label: c.localizedString("Loadpoint_ResetOnDisconnect", nil), + valueType: templates.ParamValueTypeBool, + }) c.configuration.AddLoadpoint(loadpoint) fmt.Println() diff --git a/cmd/configure/survey.go b/cmd/configure/survey.go index 941dd420b9..a4651cb676 100644 --- a/cmd/configure/survey.go +++ b/cmd/configure/survey.go @@ -8,6 +8,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2/terminal" + "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util/templates" "github.com/thoas/go-funk" ) @@ -115,6 +116,7 @@ type question struct { invalidValues []string valueType string mask, required bool + excludeNone bool } // askBoolValue asks for a boolean value selection for a given question @@ -137,6 +139,22 @@ func (c *CmdConfigure) askValue(q question) string { return c.askBoolValue(label) } + if q.valueType == templates.ParamValueTypeChargeModes { + chargingModes := []string{string(api.ModeOff), string(api.ModeNow), string(api.ModeMinPV), string(api.ModePV)} + chargeModes := []string{ + c.localizedString("ChargeModeOff", nil), + c.localizedString("ChargeModeNow", nil), + c.localizedString("ChargeModeMinPV", nil), + c.localizedString("ChargeModePV", nil), + } + if !q.excludeNone { + chargingModes = append(chargingModes, "") + chargeModes = append(chargeModes, c.localizedString("ChargeModeNone", nil)) + } + modeChoice, _ := c.askChoice(c.localizedString("ChargeMode_Question", nil), chargeModes) + return chargingModes[modeChoice] + } + input := "" var err error @@ -174,7 +192,7 @@ func (c *CmdConfigure) askValue(q question) string { } else { help += " (" + c.localizedString("Value_Optional", nil) + ")" } - if q.exampleValue != "" { + if q.exampleValue != nil && q.exampleValue != "" { help += fmt.Sprintf(" ("+c.localizedString("Value_Sample", nil)+": %s)", q.exampleValue) } diff --git a/cmd/configure/texts.go b/cmd/configure/texts.go index ba373fa49e..42102ebc3b 100644 --- a/cmd/configure/texts.go +++ b/cmd/configure/texts.go @@ -49,49 +49,49 @@ func (c *CmdConfigure) userFriendlyTexts(param templates.Param) templates.Param result.ValueType = templates.ParamValueTypeString } - switch strings.ToLower(result.Name) { - case "title": - result.Name = c.localizedString("UserFriendly_Title_Name", nil) - if result.Help.String(c.lang) == "" { - result.Help.SetString(c.lang, c.localizedString("UserFriendly_Title_Help", nil)) - } - case "device": - result.Name = c.localizedString("UserFriendly_Device_Name", nil) - case "baudrate": - result.Name = c.localizedString("UserFriendly_Baudrate_Name", nil) - case "comset": - result.Name = c.localizedString("UserFriendly_ComSet_Name", nil) - case "host": - result.Name = c.localizedString("UserFriendly_Host_Name", nil) - case "port": - result.Name = c.localizedString("UserFriendly_Port_Name", nil) - result.ValueType = templates.ParamValueTypeNumber - case "user": - result.Name = c.localizedString("UserFriendly_User_Name", nil) - case "password": - result.Name = c.localizedString("UserFriendly_Password_Name", nil) - case "capacity": - result.Name = c.localizedString("UserFriendly_Capacity_Name", nil) - if result.Example == "" { - result.Example = "41.5" + type replacements struct { + Name string + Help string + Example string + ValueType string + } + resultNameMap := map[string]replacements{ + "title": {Name: "UserFriendly_Title_Name", Help: "UserFriendly_Title_Help"}, + "device": {Name: "UserFriendly_Device_Name"}, + "baudrate": {Name: "UserFriendly_Baudrate_Name"}, + "comset": {Name: "UserFriendly_ComSet_Name"}, + "host": {Name: "UserFriendly_Host_Name"}, + "port": {Name: "UserFriendly_Port_Name", ValueType: templates.ParamValueTypeNumber}, + "user": {Name: "UserFriendly_User_Name"}, + "password": {Name: "UserFriendly_Password_Name"}, + "capacity": {Name: "UserFriendly_Capacity_Name", Example: "41.5", ValueType: templates.ParamValueTypeFloat}, + "vin": {Name: "UserFriendly_Vin_Name", Help: "UserFriendly_Vin_Help"}, + "cache": {Name: "UserFriendly_Cache_Name", Help: "UserFriendly_Cache_Help", Example: "5m"}, + "mode": {Name: "ChargeMode_Question", ValueType: templates.ParamValueTypeChargeModes}, + "minsoc": {Name: "UserFriendly_MinSoC_Name", Help: "UserFriendly_MinSoC_Help", Example: "25", ValueType: templates.ParamValueTypeNumber}, + "targetsoc": {Name: "UserFriendly_TargetSoC_Name", Help: "UserFriendly_TargetSoC_Help", Example: "80", ValueType: templates.ParamValueTypeNumber}, + "mincurrent": {Name: "UserFriendly_MinCurrent_Name", Help: "UserFriendly_MinCurrent_Help", Example: "6", ValueType: templates.ParamValueTypeNumber}, + "maxcurrent": {Name: "UserFriendly_MaxCurrent_Name", Help: "UserFriendly_MaxCurrent_Help", Example: "16", ValueType: templates.ParamValueTypeNumber}, + "identifiers": {Name: "UserFriendly_Identifier_Name", Help: "UserFriendly_Identifier_Help"}, + "standbypower": {Name: "UserFriendly_StandByPower_Name", Help: "UserFriendly_StandByPower_Help", ValueType: templates.ParamValueTypeNumber}, + } + + if resultMapItem := resultNameMap[strings.ToLower(result.Name)]; resultMapItem != (replacements{}) { + // always overwrite if defined + if resultMapItem.Name != "" { + result.Name = c.localizedString(resultMapItem.Name, nil) } - result.ValueType = templates.ParamValueTypeFloat - case "vin": - result.Name = c.localizedString("UserFriendly_Vin_Name", nil) - if result.Help.String(c.lang) == "" { - result.Help.SetString(c.lang, c.localizedString("UserFriendly_Vin_Help", nil)) + if resultMapItem.ValueType != "" { + result.ValueType = resultMapItem.ValueType } - 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)) + // only set if empty + if result.Help.String(c.lang) == "" && resultMapItem.Help != "" { + result.Help.SetString(c.lang, c.localizedString(resultMapItem.Help, nil)) } - case "standbypower": - result.Name = c.localizedString("UserFriendly_StandByPower_Name", nil) - if result.Help.String(c.lang) == "" { - result.Help.SetString(c.lang, c.localizedString("UserFriendly_StandByPower_Help", nil)) + if result.Example == "" && resultMapItem.Example != "" { + result.Example = resultMapItem.Example } - result.ValueType = templates.ParamValueTypeNumber } + return result } diff --git a/cmd/configure/types.go b/cmd/configure/types.go index b9ad56f4f7..5757e1f35b 100644 --- a/cmd/configure/types.go +++ b/cmd/configure/types.go @@ -1,7 +1,5 @@ package configure -import "github.com/evcc-io/evcc/util/templates" - const ( DefaultConfigFilename string = "evcc.yaml" ) @@ -12,8 +10,6 @@ func (u UsageChoice) String() string { return string(u) } -var ValidModbusChoices = []string{templates.ModbusChoiceRS485, templates.ModbusChoiceTCPIP} - type DeviceClass string func (c DeviceClass) String() string { @@ -42,8 +38,6 @@ const ( DeviceCategoryGuidedSetup DeviceCategory = "guided" ) -var ValidUsageChoices = []DeviceCategory{DeviceCategoryGridMeter, DeviceCategoryPVMeter, DeviceCategoryBatteryMeter, DeviceCategoryChargeMeter} - const ( defaultNameCharger = "wallbox" defaultNameGridMeter = "grid" diff --git a/templates/README.md b/templates/README.md index 162b75d86d..7b7a9aa1f2 100644 --- a/templates/README.md +++ b/templates/README.md @@ -126,6 +126,7 @@ Example Use Case: With SMA Home Manager, there can be a SMA Energy Meter used fo - `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` +- `chargemodes`: for a selection of charge modes (including `None` which results in the param not being set) ### `advanced` diff --git a/templates/definition/charger/abl.yaml b/templates/definition/charger/abl.yaml index 0a140781da..f0c38274ab 100644 --- a/templates/definition/charger/abl.yaml +++ b/templates/definition/charger/abl.yaml @@ -5,6 +5,8 @@ requirements: params: - name: modbus choice: ["rs485"] + baudrate: 38400 + comset: "8E1" render: | type: abl {{include "modbus" .}} diff --git a/templates/definition/charger/cfos.yaml b/templates/definition/charger/cfos.yaml index ff119d528c..4f0ed7e640 100644 --- a/templates/definition/charger/cfos.yaml +++ b/templates/definition/charger/cfos.yaml @@ -3,8 +3,9 @@ description: cFos PowerBrain requirements: sponsorship: true params: -- name: modbus - choice: ["tcpip"] +- name: host + required: true + example: 192.0.2.2 render: | type: cfos - {{include "modbus" .}} + uri: {{ .host }} diff --git a/templates/definition/charger/goe.yaml b/templates/definition/charger/goe.yaml index 564563f13a..634cad2a47 100644 --- a/templates/definition/charger/goe.yaml +++ b/templates/definition/charger/goe.yaml @@ -2,8 +2,8 @@ template: go-e description: go-eCharger Home/Pro/V3 requirements: description: - en: Models Home and Pro required firmware 040.0 or later. - de: Bei den Modellen Home und Pro ist mindestens Firmware 040.0 oder höher erforderlich. + en: Home und PRO require firmware 040.0 or later, V3 requires firmware 052.1 or later. + de: Home und PRO benötigen mindestens Firmware 040.0 oder neuer, V3 benötigt mindestens Firmware 052.1 oder neuer. uri: https://docs.evcc.io/docs/devices/chargers#go-echarger-homeprov3 params: - name: host diff --git a/templates/definition/charger/heidelberg.yaml b/templates/definition/charger/heidelberg.yaml index ec9b206831..b212b719bb 100644 --- a/templates/definition/charger/heidelberg.yaml +++ b/templates/definition/charger/heidelberg.yaml @@ -5,6 +5,8 @@ requirements: params: - name: modbus choice: ["rs485"] + baudrate: 19200 + comset: "8E1" render: | type: heidelberg {{include "modbus" .}} diff --git a/templates/definition/meter/cfos.yaml b/templates/definition/meter/cfos.yaml index 24b6771aa2..961791831d 100644 --- a/templates/definition/meter/cfos.yaml +++ b/templates/definition/meter/cfos.yaml @@ -1,12 +1,14 @@ template: cfos-meter -description: cFos PowerBrain +description: cFos PowerBrain Meter requirements: sponsorship: true params: - name: usage - choice: [ "charger" ] + choice: [ "charge" ] - name: modbus - choice: ["tcpip"] + choice: [ "tcpip" ] + port: 4702 + id: 2 render: | type: cfos - {{include "modbus" .}} + {{ include "modbus" . }} diff --git a/templates/definition/meter/fritzdect.yaml b/templates/definition/meter/fritzdect.yaml new file mode 100644 index 0000000000..e10873518d --- /dev/null +++ b/templates/definition/meter/fritzdect.yaml @@ -0,0 +1,28 @@ +template: fritzdect +description: FritzDECT +params: +- name: usage + choice: ["pv", "charge"] +- name: uri + default: https://fritz.box +- name: user + required: true +- name: password + required: true + mask: true +- name: ain + required: true + mask: true + example: '007788992233' + help: + en: The AIN is printed on the type label on the back of the device. + de: Die AIN ist auf dem Typenschild auf der Geräterückseite aufgedruckt. +- name: standbypower + default: 15 +render: | + type: fritzdect + uri: {{ .uri }} + user: {{ .user }} + password: {{ .password }} + ain: {{ .ain }} # switch actor identification number without blanks (see AIN number on switch sticker) + standbypower: {{ .standbypower }} # treat as charging above this power diff --git a/templates/definition/meter/huawei-sun2000-8ktl.yaml b/templates/definition/meter/huawei-sun2000-8ktl.yaml index 40c50e8e9a..2f1c667fda 100644 --- a/templates/definition/meter/huawei-sun2000-8ktl.yaml +++ b/templates/definition/meter/huawei-sun2000-8ktl.yaml @@ -5,6 +5,7 @@ params: choice: [ "pv" ] - name: modbus choice: ["rs485"] + baudrate: 19200 render: | type: custom power: diff --git a/templates/definition/meter/kostal-energy-meter-inverter.yaml b/templates/definition/meter/kostal-energy-meter-inverter.yaml index a67519b9b9..aeb094e04e 100644 --- a/templates/definition/meter/kostal-energy-meter-inverter.yaml +++ b/templates/definition/meter/kostal-energy-meter-inverter.yaml @@ -3,17 +3,15 @@ description: Kostal Piko (BA) Energy Meter params: - name: usage choice: [ "grid" ] -- name: host - example: 192.0.2.2 - required: true -- name: port - default: 1502 +- name: modbus + choice: [ "tcpip" ] + port: 1502 + id: 71 render: | type: custom power: source: modbus # use ModBus plugin - uri: {{ .host }}:{{ .port }} # inverter port - id: 71 + {{ include "modbus" . | indent 2 }} register: # manual non-sunspec register configuration address: 252 # (see ba_kostal_interface_modbus-tcp_sunspec.pdf) type: holding diff --git a/templates/definition/meter/kostal-plenticore.yaml b/templates/definition/meter/kostal-plenticore.yaml index b67e4adeff..1a9e775415 100644 --- a/templates/definition/meter/kostal-plenticore.yaml +++ b/templates/definition/meter/kostal-plenticore.yaml @@ -8,16 +8,14 @@ guidedsetup: params: - name: usage choice: [ "pv", "battery" ] -- name: host - example: 192.0.2.2 - required: true -- name: port - default: 1502 +- name: modbus + choice: [ "tcpip" ] + id: 71 + port: 1502 render: | type: modbus model: sunspec - uri: {{ .host }}:{{ .port }} - id: 71 # kostal default sunspec modbus id + {{ include "modbus" . }} {{- if eq .usage "battery" }} power: 802:W # sunspec model 802 battery soc: 802:SoC # sunspec model 802 battery diff --git a/templates/definition/meter/kostal-smart-energy-meter.yaml b/templates/definition/meter/kostal-smart-energy-meter.yaml index fc3192f6db..304f689123 100644 --- a/templates/definition/meter/kostal-smart-energy-meter.yaml +++ b/templates/definition/meter/kostal-smart-energy-meter.yaml @@ -3,13 +3,10 @@ description: Kostal Smart Energy Meter params: - name: usage choice: [ "grid" ] -- name: host - example: 192.0.2.2 - required: true -- name: port - default: 502 +- name: modbus + choice: [ "tcpip" ] + id: 71 render: | type: modbus model: sunspec - uri: {{ .host }}:{{ .port }} - id: 71 # kostal default sunspec modbus id + {{ include "modbus" . }} diff --git a/templates/definition/meter/powerdog.yaml b/templates/definition/meter/powerdog.yaml index 487ba83004..9465275610 100644 --- a/templates/definition/meter/powerdog.yaml +++ b/templates/definition/meter/powerdog.yaml @@ -5,11 +5,8 @@ guidedsetup: params: - name: usage choice: [ "grid", "pv" ] -- name: host - required: true - example: 192.0.2.2 -- name: port - default: 502 +- name: modbus + choice: [ "tcpip" ] render: | type: custom power: @@ -17,15 +14,13 @@ render: | source: calc #calculate current overall consumption + (current pv effort * (-1) ) add: - source: modbus - uri: {{ .host }}:{{ .port }} #ip-adress and port (default-port: 502) - id: 1 + {{ include "modbus" . | indent 4 }} register: address: 40026 #register for overall consumption type: holding decode: int32 - source: modbus - uri: {{ .host }}:{{ .port }} #ip-adress and port (default-port: 502) - id: 1 + {{ include "modbus" . | indent 4 }} register: address: 40002 #register for pv effort type: holding @@ -34,8 +29,7 @@ render: | {{- end }} {{- if eq .usage "pv" }} type: modbus - uri: {{ .host }}:{{ .port }} #ip-adress and port (default-port: 502) - id: 1 + {{ include "modbus" . | indent 2 }} register: address: 40002 #register for pv effort type: holding diff --git a/templates/definition/meter/sunspec.yaml b/templates/definition/meter/sunspec.yaml index cc74fe8d25..6d866ec5eb 100644 --- a/templates/definition/meter/sunspec.yaml +++ b/templates/definition/meter/sunspec.yaml @@ -4,17 +4,12 @@ generic: true params: - name: usage choice: [ "grid", "pv", "battery" ] -- name: host - example: 192.0.2.2 - required: true -- name: id - default: 1 - valuetype: number +- name: modbus + choice: [ "tcpip" ] render: | type: modbus model: sunspec - uri: {{ .host }} - id: {{ .id }} + {{ include "modbus" .}} {{- if eq .usage "grid" }} power: 203:W # sunspec 3-phase meter power reading {{- end -}} diff --git a/templates/definition/parambaselist.yaml b/templates/definition/parambaselist.yaml index 7f8ab85460..45d6aa0f6e 100644 --- a/templates/definition/parambaselist.yaml +++ b/templates/definition/parambaselist.yaml @@ -10,7 +10,18 @@ vehicle: example: W... - name: capacity default: '50' - valueType: float + - name: cache + advanced: true + - name: mode + advanced: true + - name: minSoC + advanced: true + - name: targetSoC + advanced: true + - name: minCurrent + advanced: true + - name: maxCurrent + advanced: true - name: identifiers advanced: true valueType: stringlist @@ -27,6 +38,27 @@ vehicle: {{- if ne .vin "" }} vin: {{ .vin }} {{- end }} + {{- if ne .cache "" }} + cache: {{ .cache }} + {{- end }} + {{- if or (ne .mode "") (ne .minSoC "") (ne .targetSoC "") (ne .minCurrent "") (ne .maxCurrent "") }} + onIdentify: + {{- if (ne .mode "") }} + mode: {{ .mode }} + {{- end }} + {{- if (ne .minSoC "") }} + minSoC: {{ .minSoC }} + {{- end }} + {{- if (ne .targetSoC "") }} + targetSoC: {{ .targetSoC }} + {{- end }} + {{- if (ne .minCurrent "") }} + minCurrent: {{ .minCurrent }} + {{- end }} + {{- if (ne .maxCurrent "") }} + maxCurrent: {{ .maxCurrent }} + {{- end }} + {{- end }} {{- if ne (len .identifiers) 0 }} identifiers: {{- range .identifiers }} diff --git a/templates/docs/charger/cfos_charger.yaml b/templates/docs/charger/cfos_charger.yaml index cbc76cbce5..29f6afacf2 100644 --- a/templates/docs/charger/cfos_charger.yaml +++ b/templates/docs/charger/cfos_charger.yaml @@ -1,3 +1,4 @@ type: template template: cfos_charger -description: cFos PowerBrain \ No newline at end of file +description: cFos PowerBrain +host: 192.0.2.2 \ No newline at end of file diff --git a/templates/docs/meter/cfos-meter-charge.yaml b/templates/docs/meter/cfos-meter-charge.yaml new file mode 100644 index 0000000000..716790ce8d --- /dev/null +++ b/templates/docs/meter/cfos-meter-charge.yaml @@ -0,0 +1,4 @@ +type: template +template: cfos-meter +description: cFos PowerBrain Meter +usage: charge \ No newline at end of file diff --git a/templates/docs/meter/cfos-meter-charger.yaml b/templates/docs/meter/cfos-meter-charger.yaml deleted file mode 100644 index e654d99ca3..0000000000 --- a/templates/docs/meter/cfos-meter-charger.yaml +++ /dev/null @@ -1,4 +0,0 @@ -type: template -template: cfos-meter -description: cFos PowerBrain -usage: charger \ No newline at end of file diff --git a/templates/docs/meter/fritzdect-charge.yaml b/templates/docs/meter/fritzdect-charge.yaml new file mode 100644 index 0000000000..59d217693b --- /dev/null +++ b/templates/docs/meter/fritzdect-charge.yaml @@ -0,0 +1,9 @@ +type: template +template: fritzdect +description: FritzDECT +usage: charge +uri: https://fritz.box +user: +password: +ain: 007788992233 # Die AIN ist auf dem Typenschild auf der Geräterückseite aufgedruckt. +standbypower: 15 \ No newline at end of file diff --git a/templates/docs/meter/fritzdect-pv.yaml b/templates/docs/meter/fritzdect-pv.yaml new file mode 100644 index 0000000000..99f87626a1 --- /dev/null +++ b/templates/docs/meter/fritzdect-pv.yaml @@ -0,0 +1,9 @@ +type: template +template: fritzdect +description: FritzDECT +usage: pv +uri: https://fritz.box +user: +password: +ain: 007788992233 # Die AIN ist auf dem Typenschild auf der Geräterückseite aufgedruckt. +standbypower: 15 \ No newline at end of file diff --git a/templates/docs/meter/kostal-piko-energy-meter-grid.yaml b/templates/docs/meter/kostal-piko-energy-meter-grid.yaml index 9792e22d36..f88c1f4819 100644 --- a/templates/docs/meter/kostal-piko-energy-meter-grid.yaml +++ b/templates/docs/meter/kostal-piko-energy-meter-grid.yaml @@ -1,6 +1,4 @@ type: template template: kostal-piko-energy-meter description: Kostal Piko (BA) Energy Meter -usage: grid -host: 192.0.2.2 -port: 1502 \ No newline at end of file +usage: grid \ No newline at end of file diff --git a/templates/docs/meter/kostal-plenticore-battery.yaml b/templates/docs/meter/kostal-plenticore-battery.yaml index cfcad019a8..4cd830c69b 100644 --- a/templates/docs/meter/kostal-plenticore-battery.yaml +++ b/templates/docs/meter/kostal-plenticore-battery.yaml @@ -1,6 +1,4 @@ type: template template: kostal-plenticore description: Kostal Plenticore Hybrid -usage: battery -host: 192.0.2.2 -port: 1502 \ No newline at end of file +usage: battery \ No newline at end of file diff --git a/templates/docs/meter/kostal-plenticore-pv.yaml b/templates/docs/meter/kostal-plenticore-pv.yaml index 5814c1c727..6f481e5622 100644 --- a/templates/docs/meter/kostal-plenticore-pv.yaml +++ b/templates/docs/meter/kostal-plenticore-pv.yaml @@ -1,6 +1,4 @@ type: template template: kostal-plenticore description: Kostal Plenticore Hybrid -usage: pv -host: 192.0.2.2 -port: 1502 \ No newline at end of file +usage: pv \ No newline at end of file diff --git a/templates/docs/meter/kostal-smart-energy-meter-grid.yaml b/templates/docs/meter/kostal-smart-energy-meter-grid.yaml index eb87f94e55..5a6cf0a997 100644 --- a/templates/docs/meter/kostal-smart-energy-meter-grid.yaml +++ b/templates/docs/meter/kostal-smart-energy-meter-grid.yaml @@ -1,6 +1,4 @@ type: template template: kostal-smart-energy-meter description: Kostal Smart Energy Meter -usage: grid -host: 192.0.2.2 -port: 502 \ No newline at end of file +usage: grid \ No newline at end of file diff --git a/templates/docs/meter/powerdog-grid.yaml b/templates/docs/meter/powerdog-grid.yaml index b5725d58f4..ea4be2cc47 100644 --- a/templates/docs/meter/powerdog-grid.yaml +++ b/templates/docs/meter/powerdog-grid.yaml @@ -1,6 +1,4 @@ type: template template: powerdog description: Powerdog -usage: grid -host: 192.0.2.2 -port: 502 \ No newline at end of file +usage: grid \ No newline at end of file diff --git a/templates/docs/meter/powerdog-pv.yaml b/templates/docs/meter/powerdog-pv.yaml index ea0d86a83c..fa665bfc0d 100644 --- a/templates/docs/meter/powerdog-pv.yaml +++ b/templates/docs/meter/powerdog-pv.yaml @@ -1,6 +1,4 @@ type: template template: powerdog description: Powerdog -usage: pv -host: 192.0.2.2 -port: 502 \ No newline at end of file +usage: pv \ No newline at end of file diff --git a/templates/docs/meter/sunspec-battery.yaml b/templates/docs/meter/sunspec-battery.yaml index 11563dea2c..c2e5bc5405 100644 --- a/templates/docs/meter/sunspec-battery.yaml +++ b/templates/docs/meter/sunspec-battery.yaml @@ -1,6 +1,4 @@ type: template template: sunspec description: Inverter / Wechselrichter (SunSpec) -usage: battery -host: 192.0.2.2 -id: 1 \ No newline at end of file +usage: battery \ No newline at end of file diff --git a/templates/docs/meter/sunspec-grid.yaml b/templates/docs/meter/sunspec-grid.yaml index 9c05894d8c..b90a5a35b0 100644 --- a/templates/docs/meter/sunspec-grid.yaml +++ b/templates/docs/meter/sunspec-grid.yaml @@ -1,6 +1,4 @@ type: template template: sunspec description: Inverter / Wechselrichter (SunSpec) -usage: grid -host: 192.0.2.2 -id: 1 \ No newline at end of file +usage: grid \ No newline at end of file diff --git a/templates/docs/meter/sunspec-pv.yaml b/templates/docs/meter/sunspec-pv.yaml index d39367cc68..e5770741ee 100644 --- a/templates/docs/meter/sunspec-pv.yaml +++ b/templates/docs/meter/sunspec-pv.yaml @@ -1,6 +1,4 @@ type: template template: sunspec description: Inverter / Wechselrichter (SunSpec) -usage: pv -host: 192.0.2.2 -id: 1 \ No newline at end of file +usage: pv \ No newline at end of file diff --git a/util/templates/generate/generate.go b/util/templates/generate/generate.go index a64b8a36d2..5e8e2a76a2 100644 --- a/util/templates/generate/generate.go +++ b/util/templates/generate/generate.go @@ -34,6 +34,9 @@ func main() { func generateClass(class string) error { for _, tmpl := range templates.ByClass(class) { + if err := tmpl.Validate(); err != nil { + return err + } usages := tmpl.Usages() fmt.Println(tmpl.Template) diff --git a/util/templates/init.go b/util/templates/init.go index 621f81d9b2..b55ea14b21 100644 --- a/util/templates/init.go +++ b/util/templates/init.go @@ -39,9 +39,12 @@ func loadTemplates(class string) { var tmpl Template if err = yaml.Unmarshal(b, &tmpl); err != nil { - panic(fmt.Errorf("reading template '%s' failed: %w", filepath, err)) + return fmt.Errorf("reading template '%s' failed: %w", filepath, err) } tmpl.ResolveParamBase() + if err = tmpl.Validate(); err != nil { + return err + } path := path.Dir(filepath) templates[path] = append(templates[path], tmpl) diff --git a/util/templates/template.go b/util/templates/template.go index 8848ab0b76..6c2a15d031 100644 --- a/util/templates/template.go +++ b/util/templates/template.go @@ -10,6 +10,7 @@ import ( "github.com/Masterminds/sprig/v3" "github.com/evcc-io/evcc/templates/definition" "github.com/evcc-io/evcc/util" + "github.com/thoas/go-funk" "gopkg.in/yaml.v3" ) @@ -17,6 +18,11 @@ const ( ParamUsage = "usage" ParamModbus = "modbus" + UsageChoiceGrid = "grid" + UsageChoicePV = "pv" + UsageChoiceBattery = "battery" + UsageChoiceCharge = "charge" + HemsTypeSMA = "sma" ModbusChoiceRS485 = "rs485" @@ -48,14 +54,18 @@ const ( var HemsValueTypes = []string{HemsTypeSMA} const ( - ParamValueTypeString = "string" - ParamValueTypeNumber = "number" - ParamValueTypeFloat = "float" - ParamValueTypeBool = "bool" - ParamValueTypeStringList = "stringlist" + ParamValueTypeString = "string" + ParamValueTypeNumber = "number" + ParamValueTypeFloat = "float" + ParamValueTypeBool = "bool" + ParamValueTypeStringList = "stringlist" + ParamValueTypeChargeModes = "chargemodes" ) -var ParamValueTypes = []string{ParamValueTypeString, ParamValueTypeNumber, ParamValueTypeBool} +var ParamValueTypes = []string{ParamValueTypeString, ParamValueTypeNumber, ParamValueTypeFloat, ParamValueTypeBool, ParamValueTypeStringList, ParamValueTypeChargeModes} + +var ValidModbusChoices = []string{ModbusChoiceRS485, ModbusChoiceTCPIP} +var ValidUsageChoices = []string{UsageChoiceGrid, UsageChoicePV, UsageChoiceBattery, UsageChoiceCharge} // language specific texts type TextLanguage struct { @@ -123,6 +133,8 @@ type Param struct { Usages []string Baudrate int // device specific default for modbus RS485 baudrate Comset string // device specific default for modbus RS485 comset + Port int // device specific default for modbus TCPIP port + ID int // device specific default for modbus ID } type ParamBase struct { @@ -144,6 +156,27 @@ type Template struct { Render string // rendering template } +func (t *Template) Validate() error { + for _, p := range t.Params { + switch p.Name { + case ParamUsage: + for _, c := range p.Choice { + if !funk.ContainsString(ValidUsageChoices, c) { + return fmt.Errorf("invalid usage choice '%s' in template %s", c, t.Template) + } + } + case ParamModbus: + for _, c := range p.Choice { + if !funk.ContainsString(ValidModbusChoices, c) { + return fmt.Errorf("invalid modbus choice '%s' in template %s", c, t.Template) + } + } + } + } + + return nil +} + // add the referenced base Params and overwrite existing ones func (t *Template) ResolveParamBase() { if t.ParamsBase == "" { @@ -190,6 +223,8 @@ func (t *Template) Defaults(docsOrTests bool) map[string]interface{} { switch p.ValueType { case ParamValueTypeStringList: values[p.Name] = []string{} + case ParamValueTypeChargeModes: + values[p.Name] = "" default: if p.Test != "" { values[p.Name] = p.Test @@ -234,17 +269,15 @@ func (t *Template) ModbusChoices() []string { //go:embed proxy.tpl var proxyTmpl string -// RenderProxy renders the proxy template for inclusion in documentation -func (t *Template) RenderProxy() ([]byte, error) { - return t.RenderProxyWithValues(nil, false) -} - +// RenderProxy renders the proxy template func (t *Template) RenderProxyWithValues(values map[string]interface{}, includeDescription bool) ([]byte, error) { tmpl, err := template.New("yaml").Funcs(template.FuncMap(sprig.FuncMap())).Parse(proxyTmpl) if err != nil { panic(err) } + t.ModbusParams(values) + for index, p := range t.Params { for k, v := range values { if p.Name != k { @@ -257,7 +290,12 @@ func (t *Template) RenderProxyWithValues(values map[string]interface{}, includeD t.Params[index].Values = append(p.Values, yamlQuote(e)) } default: - t.Params[index].Value = yamlQuote(v.(string)) + switch v := v.(type) { + case string: + t.Params[index].Value = yamlQuote(v) + case int: + t.Params[index].Value = fmt.Sprintf("%d", v) + } } } } diff --git a/util/templates/template_modbus.go b/util/templates/template_modbus.go index 59eac61c65..9a286a7d78 100644 --- a/util/templates/template_modbus.go +++ b/util/templates/template_modbus.go @@ -12,6 +12,41 @@ import ( //go:embed modbus.tpl var modbusTmpl string +// add the modbus params to the template +func (t *Template) ModbusParams(values map[string]interface{}) { + if len(t.ModbusChoices()) == 0 { + return + } + + if values[ParamModbus] == nil { + return + } + + for k := range values { + if k == ModbusParamNameId { + t.Params = append(t.Params, Param{Name: ModbusParamNameId, ValueType: ParamValueTypeNumber}) + continue + } + + type modbusData struct { + modbusKeys []string + valueType string + } + paramMap := map[string]modbusData{ + ModbusParamNameId: {modbusKeys: []string{ModbusKeyTCPIP, ModbusKeyRS485TCPIP, ModbusKeyRS485Serial}, valueType: ParamValueTypeNumber}, + ModbusParamNameHost: {modbusKeys: []string{ModbusKeyTCPIP, ModbusKeyRS485TCPIP}, valueType: ParamValueTypeString}, + ModbusParamNamePort: {modbusKeys: []string{ModbusKeyTCPIP, ModbusKeyRS485TCPIP}, valueType: ParamValueTypeNumber}, + ModbusParamNameDevice: {modbusKeys: []string{ModbusKeyRS485Serial}, valueType: ParamValueTypeString}, + ModbusParamNameBaudrate: {modbusKeys: []string{ModbusKeyRS485Serial}, valueType: ParamValueTypeNumber}, + ModbusParamNameComset: {modbusKeys: []string{ModbusKeyRS485Serial}, valueType: ParamValueTypeString}, + } + + if funk.ContainsString(paramMap[k].modbusKeys, values[ParamModbus].(string)) { + t.Params = append(t.Params, Param{Name: k, ValueType: paramMap[k].valueType}) + } + } +} + // set the modbus values required from modbus.tpl and and the template to the render func (t *Template) ModbusValues(values map[string]interface{}) { if len(t.ModbusChoices()) == 0 {