Template improvements (#2019)

Fix #2018

- Update go-e firmware requirements
- Change cfos devices not to use modbus config schema
- Add support for `onIdentify` params of vehicles
- Add support for `resetOnDisconnect` for loadpoints
- Add support for defining device specific modbus port, baudrate and comset defaults
- Handle fatal error when `evcc.yaml` exists and the process doesn't have write permissions
- Some bug fixes
This commit is contained in:
Andreas Linde 2021-12-12 19:56:28 +01:00 • committed by GitHub
parent 69c923a749
commit 96a0475390
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 364 additions and 181 deletions

View file

@ -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 {

View file

@ -43,6 +43,7 @@ loadpoints:
phases: {{ .Phases }}
mincurrent: {{ .MinCurrent }}
maxcurrent: {{ .MaxCurrent }}
resetOnDisconnect: {{ .ResetOnDisconnect }}
{{- end }}
{{- end }}

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -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()

View file

@ -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)
}

View file

@ -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"
}
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))
}
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))
}
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))
}
result.ValueType = templates.ParamValueTypeNumber
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)
}
if resultMapItem.ValueType != "" {
result.ValueType = resultMapItem.ValueType
}
// only set if empty
if result.Help.String(c.lang) == "" && resultMapItem.Help != "" {
result.Help.SetString(c.lang, c.localizedString(resultMapItem.Help, nil))
}
if result.Example == "" && resultMapItem.Example != "" {
result.Example = resultMapItem.Example
}
}
return result
}

View file

@ -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"

View file

@ -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`

View file

@ -5,6 +5,8 @@ requirements:
params:
- name: modbus
choice: ["rs485"]
baudrate: 38400
comset: "8E1"
render: |
type: abl
{{include "modbus" .}}

View file

@ -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 }}

View file

@ -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

View file

@ -5,6 +5,8 @@ requirements:
params:
- name: modbus
choice: ["rs485"]
baudrate: 19200
comset: "8E1"
render: |
type: heidelberg
{{include "modbus" .}}

View file

@ -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" . }}

View file

@ -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

View file

@ -5,6 +5,7 @@ params:
choice: [ "pv" ]
- name: modbus
choice: ["rs485"]
baudrate: 19200
render: |
type: custom
power:

View file

@ -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

View file

@ -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

View file

@ -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" . }}

View file

@ -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

View file

@ -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 -}}

View file

@ -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 }}

View file

@ -1,3 +1,4 @@
type: template
template: cfos_charger
description: cFos PowerBrain
description: cFos PowerBrain
host: 192.0.2.2

View file

@ -0,0 +1,4 @@
type: template
template: cfos-meter
description: cFos PowerBrain Meter
usage: charge

View file

@ -1,4 +0,0 @@
type: template
template: cfos-meter
description: cFos PowerBrain
usage: charger

View file

@ -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

View file

@ -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

View file

@ -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
usage: grid

View file

@ -1,6 +1,4 @@
type: template
template: kostal-plenticore
description: Kostal Plenticore Hybrid
usage: battery
host: 192.0.2.2
port: 1502
usage: battery

View file

@ -1,6 +1,4 @@
type: template
template: kostal-plenticore
description: Kostal Plenticore Hybrid
usage: pv
host: 192.0.2.2
port: 1502
usage: pv

View file

@ -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
usage: grid

View file

@ -1,6 +1,4 @@
type: template
template: powerdog
description: Powerdog
usage: grid
host: 192.0.2.2
port: 502
usage: grid

View file

@ -1,6 +1,4 @@
type: template
template: powerdog
description: Powerdog
usage: pv
host: 192.0.2.2
port: 502
usage: pv

View file

@ -1,6 +1,4 @@
type: template
template: sunspec
description: Inverter / Wechselrichter (SunSpec)
usage: battery
host: 192.0.2.2
id: 1
usage: battery

View file

@ -1,6 +1,4 @@
type: template
template: sunspec
description: Inverter / Wechselrichter (SunSpec)
usage: grid
host: 192.0.2.2
id: 1
usage: grid

View file

@ -1,6 +1,4 @@
type: template
template: sunspec
description: Inverter / Wechselrichter (SunSpec)
usage: pv
host: 192.0.2.2
id: 1
usage: pv

View file

@ -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)

View file

@ -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)

View file

@ -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)
}
}
}
}

View file

@ -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 {