Some checks failed
Default / Build (push) Failing after 2m0s
Default / Build UI (push) Failing after 13s
Release / Publish Docker :release (push) Has been cancelled
Release / call-build-workflow (push) Has been cancelled
Release / Publish Github & APT release (push) Has been cancelled
- Configure now asks for the users knowledge level, so advanced mode can be reached without calling with `--advanced` - Add template for solaredge inverter - Add beta template for sungrow inverter - Add template for MQTT based meter - Add more template documentation - Add check and error message for invalid config properties when using templates - Option to specify language specific template descriptions - Option to make params dependent on values of other params - Option to add sponsortoken in advanced mode - Option to set minCurrent in advanced mode - Remove sponsorship requirement from Daheimladen wallbox - Multiple fixes (FritzDect meter, Kostsal Piko meter, ...)
389 lines
11 KiB
Go
389 lines
11 KiB
Go
package configure
|
|
|
|
import (
|
|
"bufio"
|
|
_ "embed"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/cloudfoundry/jibber_jabber"
|
|
"github.com/evcc-io/evcc/server"
|
|
"github.com/evcc-io/evcc/util"
|
|
"github.com/evcc-io/evcc/util/templates"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
//go:embed localization/de.toml
|
|
var lang_de string
|
|
|
|
//go:embed localization/en.toml
|
|
var lang_en string
|
|
|
|
type CmdConfigure struct {
|
|
configuration Configure
|
|
localizer *i18n.Localizer
|
|
log *util.Logger
|
|
|
|
lang string
|
|
advancedMode, expandedMode bool
|
|
addedDeviceIndex int
|
|
errItemNotPresent, errDeviceNotValid error
|
|
}
|
|
|
|
// Run starts the interactive configuration
|
|
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)
|
|
|
|
bundle := i18n.NewBundle(language.German)
|
|
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
|
if _, err := bundle.ParseMessageFileBytes([]byte(lang_de), "localization/de.toml"); err != nil {
|
|
panic(err)
|
|
}
|
|
if _, err := bundle.ParseMessageFileBytes([]byte(lang_en), "localization/en.toml"); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
c.lang = "de"
|
|
systemLanguage, err := jibber_jabber.DetectLanguage()
|
|
if err == nil {
|
|
c.lang = systemLanguage
|
|
}
|
|
if flagLang != "" {
|
|
c.lang = flagLang
|
|
}
|
|
|
|
c.localizer = i18n.NewLocalizer(bundle, c.lang)
|
|
|
|
c.setDefaultTexts()
|
|
|
|
fmt.Println()
|
|
fmt.Println(c.localizedString("Intro", nil))
|
|
|
|
if !c.advancedMode {
|
|
// ask the user for his knowledge, so advanced mode can also be turned on this way
|
|
fmt.Println()
|
|
flowIndex, _ := c.askChoice(c.localizedString("Flow_Mode", nil), []string{
|
|
c.localizedString("Flow_Mode_Standard", nil),
|
|
c.localizedString("Flow_Mode_Advanced", nil),
|
|
})
|
|
if flowIndex == 1 {
|
|
c.advancedMode = true
|
|
}
|
|
}
|
|
|
|
if !c.advancedMode {
|
|
c.flowNewConfigFile()
|
|
return
|
|
}
|
|
|
|
fmt.Println()
|
|
flowIndex, _ := c.askChoice(c.localizedString("Flow_Type", nil), []string{
|
|
c.localizedString("Flow_Type_NewConfiguration", nil),
|
|
c.localizedString("Flow_Type_SingleDevice", nil),
|
|
})
|
|
switch flowIndex {
|
|
case 0:
|
|
c.flowNewConfigFile()
|
|
case 1:
|
|
c.flowSingleDevice()
|
|
}
|
|
}
|
|
|
|
// configureSingleDevice implements the flow for getting a single device configuration
|
|
func (c *CmdConfigure) flowSingleDevice() {
|
|
fmt.Println()
|
|
fmt.Println(c.localizedString("Flow_SingleDevice_Setup", nil))
|
|
fmt.Println()
|
|
fmt.Println(c.localizedString("Flow_SingleDevice_Select", nil))
|
|
|
|
// only consider the device categories that are marked for this flow
|
|
categoryChoices := []string{
|
|
DeviceCategories[DeviceCategoryGridMeter].title,
|
|
DeviceCategories[DeviceCategoryPVMeter].title,
|
|
DeviceCategories[DeviceCategoryBatteryMeter].title,
|
|
DeviceCategories[DeviceCategoryChargeMeter].title,
|
|
DeviceCategories[DeviceCategoryCharger].title,
|
|
DeviceCategories[DeviceCategoryVehicle].title,
|
|
}
|
|
|
|
fmt.Println()
|
|
_, cagetoryTitle := c.askChoice(c.localizedString("Flow_SingleDevice_Select", nil), categoryChoices)
|
|
|
|
var selectedCategory DeviceCategory
|
|
for item, data := range DeviceCategories {
|
|
if data.title == cagetoryTitle {
|
|
selectedCategory = item
|
|
break
|
|
}
|
|
}
|
|
|
|
devices := c.configureDevices(selectedCategory, false, false)
|
|
for _, item := range devices {
|
|
fmt.Println()
|
|
fmt.Println(c.localizedString("Flow_SingleDevice_Config", localizeMap{}))
|
|
fmt.Println()
|
|
|
|
scanner := bufio.NewScanner(strings.NewReader(item.Yaml))
|
|
for scanner.Scan() {
|
|
fmt.Println(" " + scanner.Text())
|
|
}
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
// configureNewConfigFile implements the flow for creating a new configuration file
|
|
func (c *CmdConfigure) flowNewConfigFile() {
|
|
fmt.Println()
|
|
fmt.Println(c.localizedString("Flow_NewConfiguration_Setup", nil))
|
|
fmt.Println()
|
|
fmt.Println(c.localizedString("Flow_NewConfiguration_Select", localizeMap{"ItemNotPresent": c.localizedString("ItemNotPresent", nil)}))
|
|
c.configureDeviceGuidedSetup()
|
|
|
|
_ = c.configureDevices(DeviceCategoryGridMeter, true, false)
|
|
_ = c.configureDevices(DeviceCategoryPVMeter, true, true)
|
|
_ = c.configureDevices(DeviceCategoryBatteryMeter, true, true)
|
|
_ = c.configureDevices(DeviceCategoryVehicle, true, true)
|
|
c.configureLoadpoints()
|
|
c.configureSite()
|
|
|
|
if c.advancedMode && c.configuration.config.SponsorToken == "" {
|
|
_ = c.askSponsortoken(true)
|
|
}
|
|
|
|
yaml, err := c.configuration.RenderConfiguration()
|
|
if err != nil {
|
|
c.log.FATAL.Fatal(err)
|
|
}
|
|
|
|
fmt.Println()
|
|
|
|
filename := DefaultConfigFilename
|
|
|
|
for ok := true; ok; {
|
|
file, err := os.OpenFile(filename, os.O_WRONLY, 0666)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
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{
|
|
label: c.localizedString("File_NewFilename", nil),
|
|
exampleValue: "evcc_neu.yaml",
|
|
required: true})
|
|
}
|
|
|
|
err = os.WriteFile(filename, yaml, 0755)
|
|
if err != nil {
|
|
fmt.Printf("%s: ", c.localizedString("File_Error_SaveFailed", localizeMap{"FileName": filename}))
|
|
c.log.FATAL.Fatal(err)
|
|
}
|
|
fmt.Println(c.localizedString("File_SaveSuccess", localizeMap{"FileName": filename}))
|
|
}
|
|
|
|
// configureDevices asks device specfic questions
|
|
func (c *CmdConfigure) configureDevices(deviceCategory DeviceCategory, askAdding, askMultiple bool) []device {
|
|
var devices []device
|
|
|
|
if deviceCategory == DeviceCategoryGridMeter && c.configuration.MetersOfCategory(deviceCategory) > 0 {
|
|
return nil
|
|
}
|
|
|
|
localizeMap := localizeMap{
|
|
"Article": DeviceCategories[deviceCategory].article,
|
|
"Additional": DeviceCategories[deviceCategory].additional,
|
|
"Category": DeviceCategories[deviceCategory].title,
|
|
}
|
|
if askAdding {
|
|
addDeviceText := c.localizedString("AddDeviceInCategory", localizeMap)
|
|
if c.configuration.MetersOfCategory(deviceCategory) > 0 {
|
|
addDeviceText = c.localizedString("AddAnotherDeviceInCategory", localizeMap)
|
|
}
|
|
|
|
fmt.Println()
|
|
if !c.askYesNo(addDeviceText) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
for ok := true; ok; {
|
|
device, _, err := c.configureDeviceCategory(deviceCategory)
|
|
if err != nil {
|
|
break
|
|
}
|
|
devices = append(devices, device)
|
|
|
|
if !askMultiple {
|
|
break
|
|
}
|
|
|
|
fmt.Println()
|
|
if !c.askYesNo(c.localizedString("AddAnotherDeviceInCategory", localizeMap)) {
|
|
break
|
|
}
|
|
}
|
|
|
|
return devices
|
|
}
|
|
|
|
// configureLoadpoints asks loadpoint specific questions
|
|
func (c *CmdConfigure) configureLoadpoints() {
|
|
fmt.Println()
|
|
fmt.Println(c.localizedString("Loadpoint_Setup", nil))
|
|
|
|
for ok := true; ok; {
|
|
|
|
loadpointTitle := c.askValue(question{
|
|
label: c.localizedString("Loadpoint_Title", nil),
|
|
defaultValue: c.localizedString("Loadpoint_DefaultTitle", nil),
|
|
required: true})
|
|
loadpoint := loadpoint{
|
|
Title: loadpointTitle,
|
|
Phases: 3,
|
|
MinCurrent: 6,
|
|
}
|
|
|
|
charger, capabilities, err := c.configureDeviceCategory(DeviceCategoryCharger)
|
|
if err != nil {
|
|
break
|
|
}
|
|
chargerHasMeter := charger.ChargerHasMeter
|
|
|
|
loadpoint.Charger = charger.Name
|
|
|
|
if !chargerHasMeter {
|
|
if c.askYesNo(c.localizedString("Loadpoint_WallboxWOMeter", nil)) {
|
|
chargeMeter, _, err := c.configureDeviceCategory(DeviceCategoryChargeMeter)
|
|
if err == nil {
|
|
loadpoint.ChargeMeter = chargeMeter.Name
|
|
chargerHasMeter = true
|
|
}
|
|
}
|
|
}
|
|
|
|
vehicles := c.configuration.DevicesOfClass(DeviceClassVehicle)
|
|
if len(vehicles) == 1 {
|
|
loadpoint.Vehicles = append(loadpoint.Vehicles, vehicles[0].Name)
|
|
} else if len(vehicles) > 1 {
|
|
for _, vehicle := range vehicles {
|
|
if c.askYesNo(c.localizedString("Loadpoint_VehicleChargeHere", localizeMap{"Vehicle": vehicle.Title})) {
|
|
loadpoint.Vehicles = append(loadpoint.Vehicles, vehicle.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
var minValue int = 6
|
|
if capabilities.ISO151182 {
|
|
minValue = 2
|
|
}
|
|
|
|
if c.advancedMode {
|
|
minAmperage := c.askValue(question{
|
|
label: c.localizedString("Loadpoint_WallboxMinAmperage", nil),
|
|
valueType: templates.ParamValueTypeNumber,
|
|
minNumberValue: int64(minValue),
|
|
maxNumberValue: 32,
|
|
required: true})
|
|
loadpoint.MinCurrent, _ = strconv.Atoi(minAmperage)
|
|
maxAmperage := c.askValue(question{
|
|
label: c.localizedString("Loadpoint_WallboxMaxAmperage", nil),
|
|
valueType: templates.ParamValueTypeNumber,
|
|
minNumberValue: 6,
|
|
maxNumberValue: 32,
|
|
required: true})
|
|
loadpoint.MaxCurrent, _ = strconv.Atoi(maxAmperage)
|
|
|
|
if !chargerHasMeter {
|
|
phaseChoices := []string{"1", "2", "3"}
|
|
fmt.Println()
|
|
phaseIndex, _ := c.askChoice(c.localizedString("Loadpoint_WallboxPhases", nil), phaseChoices)
|
|
loadpoint.Phases = phaseIndex + 1
|
|
}
|
|
} else {
|
|
powerChoices := []string{
|
|
c.localizedString("Loadpoint_WallboxPower36kW", nil),
|
|
c.localizedString("Loadpoint_WallboxPower11kW", nil),
|
|
c.localizedString("Loadpoint_WallboxPower22kW", nil),
|
|
c.localizedString("Loadpoint_WallboxPowerOther", nil),
|
|
}
|
|
fmt.Println()
|
|
powerIndex, _ := c.askChoice(c.localizedString("Loadpoint_WallboxMaxPower", nil), powerChoices)
|
|
loadpoint.MinCurrent = minValue
|
|
switch powerIndex {
|
|
case 0:
|
|
loadpoint.MaxCurrent = 16
|
|
if !chargerHasMeter {
|
|
loadpoint.Phases = 1
|
|
}
|
|
case 1:
|
|
loadpoint.MaxCurrent = 16
|
|
if !chargerHasMeter {
|
|
loadpoint.Phases = 3
|
|
}
|
|
case 2:
|
|
loadpoint.MaxCurrent = 32
|
|
if !chargerHasMeter {
|
|
loadpoint.Phases = 3
|
|
}
|
|
case 3:
|
|
amperage := c.askValue(question{
|
|
label: c.localizedString("Loadpoint_WallboxMaxAmperage", nil),
|
|
valueType: templates.ParamValueTypeNumber,
|
|
minNumberValue: int64(minValue),
|
|
maxNumberValue: 32,
|
|
required: true})
|
|
loadpoint.MaxCurrent, _ = strconv.Atoi(amperage)
|
|
|
|
if !chargerHasMeter {
|
|
phaseChoices := []string{"1", "2", "3"}
|
|
fmt.Println()
|
|
phaseIndex, _ := c.askChoice(c.localizedString("Loadpoint_WallboxPhases", nil), phaseChoices)
|
|
loadpoint.Phases = phaseIndex + 1
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
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()
|
|
if !c.askYesNo(c.localizedString("Loadpoint_AddAnother", nil)) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// configureSite asks site specific questions
|
|
func (c *CmdConfigure) configureSite() {
|
|
fmt.Println()
|
|
fmt.Println(c.localizedString("Site_Setup", nil))
|
|
|
|
siteTitle := c.askValue(question{
|
|
label: c.localizedString("Site_Title", nil),
|
|
defaultValue: c.localizedString("Site_DefaultTitle", nil),
|
|
required: true})
|
|
c.configuration.config.Site.Title = siteTitle
|
|
}
|